找回密码
 立即注册
查看: 267|回复: 1

实时渲染管线(Real Time Rendering Pipeline)概述

[复制链接]
发表于 2023-4-4 08:39 | 显示全部楼层 |阅读模式
1 前言

渲染管线(Rendering Pipeline)的定义简单来讲,就是将应用程序的数据转换成最终显示在屏幕上的图像——这一数据处理过程。
由于目前中文社区习惯性将「Pipeline」翻译为「管线」,一些初次接触图形学的朋友会将渲染管线误认为是 GPU 中实际存在的硬件单元。按我个人的理解,「Pipeline」更应该被翻译为「流水线」。渲染管线作为一种数据处理过程,其内部分为许多个阶段模块。数据作为输入被上一个阶段模块进行处理,产生或转变成新的数据紧接着输出传递到下一个阶段模块进行处理,重复这个流程直到管线结束。同时由于渲染管线的实现通常是并发的,这就让渲染管线在概念上更接近大规模工业生产车间的流水线。
渲染管线可以说是贯穿图形学的一条主线。它定义了图形数据由抽象到具体的流程,描述了图形硬件在抽象层面的行为,因此也决定了其在图形学中的重要地位。
2 实时渲染(Real Time Rendering)vs 离线渲染(Offline Rendering)

2.1 实时渲染

如果你是一个电子游戏爱好者,你很可能听说过“即时演算”“实时渲染”等概念。这些名词强调的正是电子游戏等实时 3D 应用对于渲染的实时性要求。由于这些应用通常需要与用户进行实时交互,应用中的场景改变就被要求能够快速反映到用户,让用户拥有符合现实世界直觉的连续交互体验。
但要想让用户看到连续流畅的画面,图形系统的输出就至少需要达到每秒 24 帧的帧率。对于实时渲染系统,这个要求就显得十分苛刻。这是因为实时渲染系统的主要目标用户的平台多数是算力有限的个人计算机,在算力有限的情况下要同时保证画面高质量和实时性是相当困难的。
在不特别强调的情况下,本文以及我以后的文章所谈到的“渲染”都指“实时渲染”。
2.2 离线渲染

而离线渲染的情况就大为不同了。在电影和动画产业,由于产出的作品不需要与观众进行实时交互,制作方通常拥有充足的时间去完成画面的渲染。对他们而言,画面的质量才是首要考虑的对象。因此离线渲染通常在算力充足的图形服务器上长时间进行,1 帧渲染一天都是非常常见的情况。
在游戏产业,离线渲染也通常是过场动画等 CG 的主流选择。这么做的好处自然是能够提升游戏 CG 的质量,但很可能会让一些画面穿帮——游戏里的某个木箱由于敌人被击败而沾上了血迹,马上进入 CG 后,这个木箱就变得完好如初,甚至连敌人的遗体都不见了。这个时候“即时演算”和“实时渲染”的重要性就不言而喻了。
3 渲染技术

目前主流的渲染技术有三种:光栅化(Rasterization)、光线追踪(Ray Tracing)和体像素(Volume Pixel)。由于最后一种的应用场景主要是在医学行业,不常用在实时渲染场景,此处便不再讨论。
3.1 光栅显示器

由于矢量图形显示器的色域小、绘制能力较弱以及不普及,光栅化图形显示器长期占据着市场的绝大部分份额。光栅显示器的图像输出是不连续的。它将屏幕分成数十万甚至上千万个方形显示单元(也就是我们经常说到的分辨率与像素的概念),并用这些离散的显示单元去模拟现实世界连续的视觉图像。很显然,光栅显示器的这种原理决定了它输出的图像总会遇到“放大变模糊”的难题,也是其相对于矢量显示器的弱势。不过也正是因为这种特性,光栅显示器的色域和绘制能力有着天然的优势,并且“放大变模糊”的问题也能通过提升显示器与图像分辨率的方法暂时缓解。
3.2 光栅化

光栅化技术与光栅显示器的流行有着密切关系。由于图形最终在光栅显示器上的输出是以像素作为表现单位的,因此一个非常自然的想法就出现了:我们只需要知道这个像素对应场景中的位置,计算这个位置应该显示什么颜色和亮度就可以了。
光栅化技术应运而生。它将场景中物体的几何数据离散化为像素,再根据像素所对应场景中的位置直接计算着色(Shading),得到每个像素具体的输出颜色和亮度等信息。这种计算往往只考虑光源和像素所对应的单个物体的点的交互,不考虑现实世界中光线在多个物体之间的反射折射。因此光栅化技术十分依赖特定的光照模型进行计算,来尽可能使输出的结果更加真实。
在光栅化技术的一众光照模型中,最经典的自然是 Blinn-Phong 模型。这种模型将光照分为环境光(Ambient)、漫射光(Diffuse)和高光(Specular)等,利用不同的经验公式分门别类地计算每一种光照的结果,最后合并各种光照的结果作为最终输出,以最大可能模拟真实光照。
3.3 光线追踪

“光线追踪”一词在近两年可谓是 GPU 产商和游戏玩家最爱挂在嘴边的词。实际上光线追踪技术的提出并不比光栅化技术晚。同样是基于“只要想办法知道这个像素应该显示什么颜色”的想法,光线追踪技术的逻辑出发点更加朴素——我们在现实世界是怎么看到东西的,我们就怎么去计算像素的输出。
光线追踪技术利用“光路可逆”原理,模拟人眼向屏幕某个位置反向“发出”光线,光线在场景中与各种物体进行反射、折射,最终“射回”到场景中的光源或者无尽的黑暗中。之后再根据这个过程形成的光路,正向计算这束光线与场景中各个物体的交互最终形成的颜色和亮度,作为像素的输出。
乍一看光线追踪的确更符合物理逻辑。加入了物体间的光照交互,它相较于光栅化技术更能模拟出真实的光照。然而光线追踪技术也正是因为如此,使得光照计算量相比光栅化技术急剧增加。在某些极端情况下,计算出的光路甚至会出现“死循环”。因此在图形处理器发展的早期,光线追踪技术在实时渲染应用几乎完全输给了光栅化技术。这种情况直到近几年高性能个人消费级 GPU 产品的出现才告一段落。不过即使到现在,光线追踪技术依旧受到算力瓶颈约束,目前的实现也大都与光栅化技术进行有机结合,并辅以一些特殊算法改善光路遍历等进行性能提升。
目前主流的图形渲染系统仍然是基于光栅化技术的。
4 固定渲染管线(Fixed Rendering Pipeline)vs 可编程渲染管线(Programmable Rendering Pipeline)

在图形学发展早期,受限于图形处理器的性能,渲染管线的实现通常是固定的。人们只能调整管线中个别阶段的个别参数,实现一些简单的图形效果。随着图形处理器的发展与人们更高级的图形需求,可编程渲染管线应运而生,传统的固定渲染管线逐渐被抛弃。
可编程渲染管线允许人们手动修改和实现渲染管线中的某个阶段,使其满足特定的渲染需求。这样的高自由度很快就得到了图形行业的广泛接纳。人们通过编写和编译着色器(Shader),控制着渲染管线的特性与运行方式。由于着色器具有一定的图灵完备性,理论上人们可以通过着色器在实现了可编程渲染管线的图形处理器上完全控制整个渲染管线,甚至可以让图形处理器像 CPU 一样处理除图形以外的多种任务。但实际在图形任务上,渲染管线的很多阶段已经非常高效稳定了,几乎不会有特殊的编程需求,如光栅化阶段和裁剪阶段。因此这些阶段的实现不需要可编程,固定化实现就已经足够了。因此目前的可编程渲染管线实现实际上是固定渲染管线和可编程渲染管线的结合。
此外,固定管线也没有完全被抛弃。由于目前仍然存有一些陈旧和性能较低的设备,固定渲染管线仍然有用武之地。甚至在一些特殊需求的驱动下,人们还会使用可编程渲染管线去实现固定渲染管线的功能。
5 渲染管线的各个阶段

上图是渲染管线各阶段的概览图。具体渲染管线的实现并不一定严格会按照上图的顺序,但也不会偏离这个框架。这当中对于编程人员最重要的阶段是顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)(片元着色器又被称作像素着色器(Pixel Shader))。这两个阶段是可完全编程的,同时对实现特殊的管线渲染效果来说是至关重要的。



渲染管线的基本结构

5.1 应用阶段

应用阶段的主要“负责人”是 CPU,它在图形任务中有三个职责:将数据加载到显存(Graphics/Video Memory)中、设置渲染状态、发起 Draw Call。
CPU 作为计算机系统的核心,对于图形这种繁重的任务,它会“委托”GPU 代替它进行任务处理。因此 CPU 首先需要将需要处理的图形数据加载到显存中,否则就算 GPU 再强大,也会遇到“无米之炊”的尴尬。将数据交给 GPU 后,CPU 还要“告诉”GPU:你要工作的渲染管线是怎么样的,哪个阶段用什么着色器,这个 3D 模型使用的是什么材质和贴图等。这样 GPU 才知道 CPU 想让它用什么方式去处理这些数据。最后 CPU 发出 Draw Call,让 GPU 开始工作,得到渲染结果。
5.2 几何处理阶段

5.2.1 顶点着色器

顶点着色器是 GPU 接手渲染管线后开始工作的第一个阶段。对于存入显存的顶点数据,顶点着色器所需要做的最基本的工作便是坐标变换和传值。由于传入的顶点数据最后都要映射到屏幕上,因此顶点着色器需要将传入的顶点的坐标系空间变换为齐次裁剪空间,剩下的透视除法、归一化为设备坐标空间等工作则由管线中的固定部分完成。此外,我们可以在顶点着色器阶段进行一些额外的计算,如通过计算逐顶点光照来确定顶点颜色等。
5.2.2 曲面细分着色器(Tessellation Shader)

曲面细分着色器是一个可选着色器,它的主要任务是根据顶点着色器的输出,使用面片(Patch)来描述物体的形状,并使用一些相对简单的面片几何体来对物体表面进行细分,产生比输入数据更多的图元,以达到使物体表面看起来更平滑的效果。
5.2.3 几何着色器(Geometry Shader)

几何着色器是一个可选着色器。相对于顶点着色器,它是管线中的“新人”。它从顶点着色器或开启的曲面细分着色器中获取图元的所有顶点,并且能够随意对其进行处理,产生更多的顶点和图元。因此它能够对图元进行一些特殊的处理。
理论上几何着色器的功能非常强大。目前几何着色器主要用于分层渲染(Layered Rendering),并与变换反馈(Transform Feedback)结合使用。
5.2.4 图元装配(Primitive Assembly)

在图元装配之前的阶段处理的数据都是顶点数据,图元装配则将这些顶点与它们组成的图元信息关联起来,为后续的裁剪和光栅化工作做准备。
5.2.5 裁剪(Clipping)

裁剪是几何处理阶段的最后一个子阶段。由于我们所要绘制的视口(Viewport)通常是非常有限的,场景中的物体不能全部出现在视口中。为了节省资源,我们需要将这些不需要进行后续处理的额外部分“裁剪”掉。在边界之外的图元被完全剔除,而跨越边界的图元则会生成新的顶点,抛弃边界之外的部分。
5.2.6 光栅化

光栅化阶段分为两个子阶段:三角形设置(Triangle Setup)和三角形遍历(Triangle Traversal)。三角形设置根据图元的顶点信息计算图元边界的表达式,用以确定这个图元所覆盖像素的范围。三角形遍历则根据三角形设置计算出的范围,决定哪一些像素被图元覆盖到了,将抽象而连续的图元坐标转换成离散的像素坐标,从而得到一组片元(Fragment)。片元不是真正意义上的像素,是许多信息和状态的集合,包括像素坐标、深度信息等。这些信息和状态将在管线的后续决定像素的颜色和亮度等,使其真正转变成可以看到的像素。这个时候的片元还是不完整的,只拥有像素坐标信息,其他的信息需要让三角形遍历根据图元顶点的信息,对图元覆盖到的像素进行插值得到。
5.2.7 片元着色器

片元着色器是管线中另一个非常重要的阶段。在管线之前的过程中,我们最终得到的片元在颜色信息上是缺少细节的(尽管我们可以在顶点着色器阶段计算顶点颜色)。片元着色器需要对每一个片元的颜色进行计算,包括但不限于纹理映射和逐像素光照。此外在片元着色器阶段我们还可以在决定一个片段是否应该被绘制成一个像素,不被我们认可的片元便会被片元着色器中止处理并丢弃(Discard)。
片元着色器的处理工作主要针对片元的颜色信息,因此对于管线的渲染结果有着最为直观的影响。不过通常情况下片元着色器在管线中处理片元时,并不会影响当前片元的邻近片元,这是片元着色器的一个局限性。同时由于在实际渲染环境下,每一个帧都拥有数百万计的片元。而输入到顶点着色器的顶点数量一般而言则是远低于百万级别,因此片元着色器的运算任务往往比顶点着色器要繁重很多,造成更大的开销。所以直接减少片元着色器的运算量,或者将片元着色器的一些运算转移到顶点着色器,依靠光栅化阶段的插值间接减少片元着色器的运算量,是提升管线性能的常用方法。
5.2.8 逐片元操作(Per-Fragment Operation)

“逐片元操作”这个名字非常直观说明了这个阶段的操作对象——每一个片元。但是这个阶段和片元着色器一样有另外一个名字——输出合并阶段(Output-Merger),这个名字说明了这个阶段最重要的操作方式——合并。合并是指的将片元的颜色与颜色缓冲区中对应位置的颜色信息进行合并,使片元成为真正意义上的像素。不过在此之前,我们还要对输入的片元进行测试。
尽管在不同的管线实现中,片元测试的内容和细节都很复杂且有很大的差异,但最基本的测试过程包括模板测试和深度测试。模板测试从模板缓冲区中读取片元对应位置的模板值,与使用读取掩码读取到的参考值进行比较。这个比较函数可以由编程人员指定,只要模板值没有通过与参考值的比较,那么该片元就会被丢弃。
接下来的深度测试与模板测试一样,是高度可配置的。深度测试开启后,会读取当前片元的深度值,并与深度缓存区中该片元对应位置的参考深度值进行比较。这里的比较函数也可以由编程人员指定,但在一般情况下,我们希望保留深度值小的片元,丢弃深度值大的片元,以达到现实世界中物体遮挡的视觉直观。如果开启了深度写入,通过了深度测试的片元的深度值就会替代原有的深度参考值,写入到深度缓冲区中。
通过了所有测试后的片元即将进入管线的结束阶段——合并。由于片元的颜色最终要写入到颜色缓冲区,而颜色缓存区中往往存储着上一次渲染结果的颜色值,我们应该直接将片元的颜色替换掉缓冲区的颜色,还是做一些其他操作?在这里大部分的管线实现提供了混合(Blend)选项。关闭混合选项,片元的颜色就会直接替换掉缓冲区的颜色。但为了实现一些特殊的渲染效果,如透明物体,我们就需要开启混合选项,指定一个混合函数,让片元的颜色值与缓冲区的颜色值进行混合,形成新的颜色值并写入到缓冲区中。
片元的颜色写入到颜色缓冲区后,它就只需要等待 GPU 将当前的颜色缓冲区与前台颜色缓冲区进行交换,使当前颜色缓冲区变成前台颜色缓冲区,便可以成为像素显示在屏幕上了。
6 图形 API

在构建完渲染管线的全貌后,我们貌似就可以直接可以动手开始实现属于我们自己的渲染管线了。然而事情并没有这么简单。同 CPU 一样,与 GPU 打交道同样需要与显存、寄存器和总线等硬件结构打交道。这对于想要开发一款 3D 应用程序的一般的编程人员而言,不仅这样的学习成本高昂,开发和维护的成本也难以让人承受。类似于使用 Linux 或 Windows 与 CPU 等硬件打交道,一些组织和企业制定了图形 API (Graphics Application Program Interface)标准。这些图形 API 抽象了对 GPU 硬件的复杂操作,同时提供了渲染管线的抽象实现,以便应用程序开发者能够以较低的成本与 GPU 进行交互。
目前主流的三大图形 API 分别为 OpenGL、DirectX 和 Vulkan。
6.1 OpenGL 与 OpenGL ES

OpenGL 诞生于 20 世纪 90 年代初,可谓是图形 API 的先行者。由图形行业龙头 Khronos 组织制定(其最初的制定者是 SGI 公司)的它由于拥有强大的可移植性与易于使用的硬件访问方式,受到了图形行业的广泛支持和应用。同时其子集 OpenGL ES 在移动嵌入式平台更是占据行业标准地位。
目前 OpenGL 的最新版本为 OpenGL 4.6,OpenGL ES 的最新版本为 OpenGL ES 3.2。
6.2 DirectX

DirectX 是由 Microsoft 制定的图形 API 标准——这样说其实并不准确。DirectX 实际上是一种多媒体 API 标准,它包含了 DirectSound(捕获和播放数字声音)、DirectMusic(数字音频处理)、DirectInput(人机交互设备处理)、DirectPlay(游戏网络通信)、DirectShow(捕获和播放多媒体) 和 Direct3D。Direct3D 才是真正意义上的图形 API 标准,但由于 Direct3D 部分在整个 DirectX 中的内容与地位逐渐提高,在图形领域里两者就基本上被画上了等号,因此使用 DirectX 指代 Direct3D 在通常情况下也是可以的。DirectX 是一个平台相关的图形 API 标准,它只能在 Mircosoft 公司推出或支持的平台上工作,例如 Mircosoft Windows 和 Mircosoft Xbox。不过得益于 Mircosoft 在个人计算机市场的高占有率,DirectX 的低移植性不仅没有限制 DirectX 的开发与应用,反而得到了越来越多的开发人员的应用与支持,充分发挥了其高效性的特点。
目前DirectX 的最新版本为 DirectX 12。
6.3 Vulkan

Vulkan 的前身是由 AMD 公司主导开发,并在 2013 年推出的 Mantle。Mantle 相对于当时的 OpenGL 和 DirectX 11,给予了开发人员对于 GPU 硬件更多的操作空间,使开发人员能够显著提升硬件利用率与 3D 应用性能。这一特性使得 Mantle 在诞生后的两年里为其他图形 API 提供了提升性能的参考方法,如 Microsoft 的 DirectX 12 和 Apple 的 Metal。但由于 AMD 缺乏对图形行业足够的领导力与影响力,AMD 在 2015 年宣布停止维护 Mantle,交给 Khronos 组织。
由于 GPU 的可编程性越来越强,越来越多的平台开始支持图形加速、多媒体编码和深度学习等技术。这使得图形 API 需要比以往更加的灵活和高效。而发展了数十年的 OpenGL 不仅越来越难以满足在 GPU 上的这种高可编程性的需求,而且也无法发挥现代 CPU 多核多线程的性能优势。因此 Khronos 组织在 Mantle 的基础上推出了 Vulkan,并力图让其成为新一代的图形 API 行业标准。
Vulkan 的表现的确非常亮眼。它把 GPU 的大部分控制权交给开发者,以往由实现 OpenGL 的 GPU 驱动做的工作(如API验证、显存管理和线程管理)现在都交给了应用开发者。虽然这样的代价是开发和维护难度的提升,但其带来的性能效益也十分巨大。同时 Vulkan 也充分利用了现代 CPU 多核多线程的优势,进一步提升应用程序的性能。最后 Vulkan 同它的前辈 OpenGL 一样,是一个平台无关的高移植性图形 API。
这么看来 Vulkan 已经可以完全替代 OpenGL,真正成为新一代的图形 API 行业标准了。但实际上 Vulkan 实现的开发与维护相当困难,目前的应用程序开发人员也并未充分利用 Vulkan 的优势。而 OpenGL 相对而言不仅开发和维护的成本较低,学习起来相对简单,而且在目前的主流平台上 OpenGL 仍然有着良好的表现,因此 OpenGL 至今仍然受到图形行业的广泛支持。Vulkan 要成为新一代图形 API 行业标准,还有很长的路要走。
目前 Vulkan 的最新版本为 Vulkan 1.3。
7 图形驱动

图形 API 抽象了 GPU 对渲染管线的实现,但真正需要与 GPU 在硬件层面打交道的是图形驱动。图形驱动与其他驱动程序一样,是沟通应用程序 API、操作系统和硬件的“中间人”。图形驱动尽管是与硬件平台强相关的,但通常可以分为两层:用户模式驱动(User-Mode Driver)和内核模式驱动(Kernel-Mode Driver),简称为 UMD 和 KMD。
UMD 的主要工作是检验图形 API 的调用,编译着色器和 API 命令,并将其送入命令缓冲区(Command Buffer)中。而 KMD 就负责管理命令缓冲区,并将命令缓冲区中的命令送入命令处理器中执行。此外 KMD 还负责 GPU 的初始化和显存管理等工作。
8 写在最后的一些碎碎念

本文是我在知乎的第一篇笔记,因此行文上比较正式。在此之前我曾使用 Github 和 Hexo 搭建了一个属于我自己的博客网站,但最后因为维护麻烦和我个人偷懒的原因,就放弃在那上面写笔记了。
此前我在学校的一个类似于社团的游戏制作工作室活动,参加了一些学生组织的游戏项目开发,也打过一些 GameJam 比赛,接触了图形学。在本文落成的几个月前,由于学院对于企业实习的学分要求,我以技术向技术美术岗位的目标向许多游戏公司投递实习,结果因为自己的准备不足,以及就业难,没有得到任何回应(也就是大家常说的简历沉池子,没有笔试面试通知,也没有被拒绝的答复)。最后历经周转,最终在某硬件产商的图形驱动部门落实了实习。
没能落实游戏行业内的实习对于喜爱游戏的我来说是一个不小的打击,但人也不能不面对现实。好在我在现在的岗位上能够有机会再系统性地学习图形学,我想这会是一次挽回的机会。以系统性学习 OpenGL 为起点,熟悉渲染开发流程,了解现代 GPU 架构,在秋招中力求上岸渲染开发岗位,正式进入游戏行业中。
于是我在知乎这里写下学习笔记,记录自己的成长。也欢迎大家一同讨论,指谪错误,一起成长。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2023-4-4 08:41 | 显示全部楼层
才刚毕业呢,优秀啊
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-23 15:26 , Processed in 1.963627 second(s), 28 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表