第二章、渲染流水线

一、什么是渲染流水线

  • 渲染流水线的目的:由一个三维场景出发、生成(或者说渲染) 一张二维图像。

这个工作通常是由CPU和GPU共同完成的,它的输入是一个虚拟摄像机、一些光源、一些Shader以及纹理等。

  • 渲染流程的概念阶段应用阶段(Application Stage)几何阶段(Geometry Stage)光栅化阶段(Rasterizer Stage)

  • 注意这里是概念阶段,每个概念阶段包含了子流水线阶段。它是为了给一个渲染流程进行基本的功能划分而提出来的。

注意:上面的三个流水线阶段是概念流水线,是为了给渲染流程进行基本的功能划分而提出来的;下面的GPU流水线才是真正的硬件渲染流水线。


1、CPU渲染起点(应用阶段)

  • 应用阶段通常由CPU负责实现,开发者具有这个阶段的绝对控制权。
  • 应用阶段的主要任务:
    • 准备好场景数据, 例如摄像机的位置、视锥体、场景中包含了哪些模型、使用了哪些光源等等
    • 为了提高渲染性能, 我们往往需要个粗粒度剔除(culling)工作, 以把那些不可见的物体剔除出去, 这样就不需要再移交给几何阶段进行处理
    • 最后, 我们需要设置好每个模型的渲染状态。这些渲染状态包括但不限于它使用的材质(漫反射颜色、高光反射颜色)、使用的纹理、使用的Shader等。这一阶段最重要的输出是渲染所需的几何信息, 即渲染图元(rendering primitives)。通俗来讲, 渲染图元可以是点、线、三角面等。这些渲染图元将会被传递给下个阶段——几何阶段。

应用阶段大致分为下面3个阶段。

1.1、把数据加载到显存中

渲染数据的加载过程:硬盘(Hard Disk Drive, HDD)——>系统内存(Random Access Memory, RAM)——>显存(Video Random Access Memory, VRAM)

真实渲染中需要加载到显存中的数据往往更多,例如顶点的位置信息、法线方向、顶点颜色、纹理坐标等

对于某些数据来书,再被加载到显存后,CPU仍然需要访问他们(例如,我们希望CPU可以访问网格数据来进行碰撞检测),那我们就不从RAM中移除这些数据,因为重新从硬盘中加载到RAM的过程是十分费时的。


1.2、设置渲染状态

渲染状态的定义:这些状态定义了场景中的网格是怎样被渲染的。例如, 使用哪个顶点着色器(Vertex Shader)片元着色器(Fragment Shader)、光源属性、材质等。如果我们没有更改渲染状态, 那么所有的网格都将使用同一种渲染状态。


1.3、调用Draw Call

  • Draw Call简单解释:Draw Call的发起方是CPU,接收方是GPU。这个命令仅仅会指向一个需要被渲染的图元列表(primitives)而不会再包含任何材质信息——因为已经在上个阶段中完成了。
  • 当给定一个Draw Call时,GPU就会根据渲染状态和所有输入的顶点数据来进行计算,最终输出成屏幕上显示的像素。整个计算过程在GPU流水线中实现。


2、GPU流水线(几何阶段和光栅化阶段)

CPU通过调用Draw Cal来命令GPU进行渲染,最终把图元渲染到屏幕上。GPU渲染的过程就是GPU流水线。
几何阶段和光栅化阶段这两个阶段在GPU流水线上进行,开发者无法拥有绝对的控制权。

几何阶段任务:

  • 通常由GPU负责实现。
  • 负责和每个渲染图元打交道,进行逐顶点、逐多边形的操作。
  • 重要任务:把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。通过对输入的渲染图元进行多步处理后,这一阶段将会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息,并传递给下一个阶段。

光栅化阶段任务:

  • 光栅化阶段通常由GPU负责实现。
  • 光栅化阶段将会使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。
  • 光栅化阶段的主要阶段:决定每个渲染图元中的哪些像素应该被绘制在屏幕上。它需要对上一个阶段得到的逐顶点数据(例如纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理。

几何阶段和光栅化阶段可以分成若干更小的流水线阶段,这些阶段由GPU来实现,每个阶段GPU提供了不同的可配置性或可编程性。


黄色:可以配置但不是可编程;蓝色:由GPU 固定实现的,开发者没有任何控制权;实线:该Shader必须由开发者编程实现;虚线:表示该Shader 是可选的

从图中可以看出,GPU的渲染流水线接收顶点数据作为输入。这些顶点数据是由应用阶段加载到显存中,再由Draw Call指定的。这些数据随后被传递给顶点着色器。


顶点着色器(Vertex Shader)是完全可编程的,它通常用于实现顶点的空间变换、顶点着色等功能。

曲面细分着色器(Tessellation Shader)是一个可选的着色器,它用于细分图元。

几何着色器(Geometry Shader)同样是一个可选的着色器,它可以被用于执行逐图元的着色操作,或者被用于产生更多的图元。

下一个流水线阶段是裁剪(Clipping),这一阶段的目的是将那些不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片。

这个阶段是可配置的。例如,我们可以使用自定义的裁剪平面来配置裁剪区域,也可以通过指令控制裁剪三角图元的正面还是背面。

几何概念阶段的最后一个流水线阶段是屏幕映射(Screen Mapping)。这一阶段是不可配置和编程的,它负责把每个图元的坐标转换到屏幕坐标系中。


光栅化概念阶段中的三角形设置(Triangle Setup)和三角形遍历(Triangle Traversal)阶段也都是固定函数(Fixed-Function)的阶段。

接下来的片元着色器(Fragment Shader),则是完全可编程的,它用于实现逐片元(Per-Fragment)的着色操作。

最后,逐片元操作(Per-FragmentOperations)阶段负责执行很多重要的操作,例如修改颜色、深度缓冲、进行混合等,它不是可编程的,但具有很高的可配置性。


接下来,我们会对其中主要的流水线阶段进行更加详细的解释。


几何阶段:

2.1、顶点着色器(Vertex Shader)

  • 处理单位:输入进来的每个顶点
  • 独立性:不可以创建或者销毁任何顶点,而且无法得到顶点与顶点之间的关系(是否属于同一个三角网格)
  • 速度快
  • 主要工作:坐标变换和逐顶点光照,还可以输出后续阶段所需的数据。

    • 坐标变换:对顶点的坐标进行某种变换。例如从模型空间转换到齐次裁剪空间,再得到归一化的设备坐标(NDC)。

o.pos = mul(UNITY_MVP, v.position);

在Unity中使用上述代码让顶点坐标乘以MVP矩阵(模型、观测、透视),将顶点从模型空间转换到齐次裁剪空间。

需要注意的是,图2.8给出的坐标范围是OpenGL同时也是Unity 使用的NDC,它的z分量范围在[-1,1]之间,而在 DirectX中,NDC的z分量范围是[0,1]。

顶点着色器可以有不同的输出方式。最常见的输出路径是经光栅化后交给片元着色器进行处理。而在现代的Shader Model中,它还可以把数据发送给曲面细分着色器或几何着色器,感兴趣的读者可以自行了解。


2.2、裁剪(Clipping)

  • 目的:裁剪掉不在摄像机视野范围内的物体

因为已知NDC下的顶点位置、所以我们只需要将图元裁剪到单位立方体内

  • 图元和摄像机视野的关系:
    • 完全在视野内:传递给下一个流水线阶段
    • 完全在视野外:不会继续向下传播,因为它们不需要被渲染
    • 部分在视野内:裁剪
  • 和顶点着色器不同,我们无法通过编程来控制裁剪的过程,而是硬件上的固定操作,但是我们可以自定义一个裁剪操作来对这一步进行配置。

2.3、屏幕映射(Screen Mapping)

  • 任务;把每个图元的x和y坐标转换到屏幕坐标系(Screen Coordianates) 下。

  • 屏幕坐标系:二维坐标系,和用于显示画面的分辨率有很大关系。
  • 窗口坐标系:屏幕坐标系和z坐标一起构成,这些值传递给光栅化阶段。

注意OpenGL定义屏幕左下角为最小窗口坐标值,而DirectX定义左上角为窗口最小坐标值,如果你发现你得到的图像是倒转的,那么就可能是因为坐标系统不匹配造成的。


光栅化阶段:

2.4、三角形设置(Triangle Setup)

  • 目标:计算每个图元覆盖的像素及像素颜色,并输出为下一个阶段做准备。
  • 任务:上一个阶段输出的都是三角网格的顶点,即我们得到的是三角网格每条边的两个端点。但如果要得到整个三角网格对像素的覆盖情况, 我们就必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。

2.5、三角形遍历(Triangle Traversal)/扫描变换(Sean Conversion)

  • 任务:检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元(fragment)
  • 过程:三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素并使用三角网格3 个顶点的顶点信息对整个覆盖区域的像素进行插值。

  • 片元不等于像素:一个片元并不是真正意义上的像素,而是包含了很多状态的集合, 这些状态用于计算每个像素的最终颜色。这些状态包括了(但不限于)它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点信息, 例如法线、纹理坐标等。

2.6、片元着色器(Fragment Shader)/像素着色器(Pixel Shader)

  • 输入:上一个阶段对定点信息插值得到的结果,根据那些从顶点着色器中输出的数据插值得到
  • 输出:一个或多个颜色值
  • 纹理采样:在顶点着色器阶段输出每个顶点对应的纹理坐标, 然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标
  • 仅影响一个片元(除非片元着色器可以访问到导数信息)

2.7、逐片元操作(Per-Fragment Operations)/输出合并阶段(Output-Merger)

  • 主要任务:
    • 决定每个片元的可见性。这涉及了很多测试工作,例如深度测试、模板测试等,测试可以打开也可以关闭,且测试比较函数可以开发者指定。
      • 模板测试(Stencil Test):GPU读取模板缓冲区该片元的模板之与参考值进行比较。
      • 深度测试(Depth Test):GPU读取片元的深度值和已经存在深度缓冲区中的深度值进行比较。

测试的过程比较复杂,并且对于不同的API,实现细节也不尽相同。

    • 如果一个片元通过了所有的测试, 就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并, 或者说是混合。
      • 混合流程:

对于不透明物体,可以关闭混合操作,选择直接覆盖,对于半透明物体,就需要使用混合操作来让物体看起来是透明的。

    • 测试顺序并不是唯一的,逻辑上说先片元着色器再测试,但大多数GPU尽可能先测试再片元着色器。
  • GPU会使用双重缓冲(Double Buffering) 的策略。这意味着,对场景的渲染是在幕后发生的,即在后置缓冲(Back Buffer) 中。一旦场景已经被渲染到了后置缓冲中,GPU就会交换后置缓冲区和前置缓冲区(Front Buffer) 中的内容,而迁至缓冲区是之前显示在屏幕上的图像。

二、扩展

1、什么是OpenGL/DirextX

我们花了一整个章节的篇幅来讲述渲染的概念流水线以及GPU 是如何实现这些流水线的,但如果要开发者直接访问GPU是一件非常麻烦的事情,我们可能需要和各种寄存器、显存打交道。而图像编程接口在这些硬件的基础上实现了一层抽象。

OpenGL和 DirectX就是这些图像应用编程接口,这些接口用于渲染二维或三维图形。可以说,这些接口架起了上层应用程序和底层GPU的沟通桥梁。一个应用程序向这些接口发送渲染命令,而这些接口会依次向显卡驱动(Graphics Driver)发送渲染命令,这些显卡驱动是真正知道如何和GPU通信的角色,正是它们把OpenGL或者DirectX的函数调用翻译成了GPU能够听懂的语言,同时它们也负责把纹理等数据转换成GPU所支持的格式。一个比喻是,显卡驱动就是显卡的操作系统。图2.18显示了这样的关系。

概括来说,我们的应用程序运行在CPU上。应用程序可以通过调用 OpenpenGL或DirectX的图形接口将渲染所需的数据,如顶点数据、纹理数据、材质参数等数据存储在显存中的特定区域。

随后,开发者可以通过图像编程接口发出渲染命令,这些渲染命令也被称为Draw Call,它们将会被显卡驱动翻译成GPU能够理解的代码,进行真正的绘制。

由图2.18可以看出,一个显卡除了有图像处理单元 GPU外,还拥有自己的内存,这个内存通常被称为显存(Video Random Access Memory,VRAM)。GPU可以在显存中存储任何数据,但对于渲染来说一些数据类型是必需的,例如用于屏幕显示的图像缓冲、深度缓冲等。

因为显卡驱动的存在,几乎所有的GPU都既可以和OpenGL 合作,也可以和 DirectX一起上作。从显卡的角度出发,实际上它只需要和显卡驱动打交道就可以了。而显卡驱动就好像一个中介者,负责和两方(图像编程接口和GPU)打交道。因此,一个显卡制作商为了让他们的显卡可以同时和OpenGL、DirectX合作,就必须提供支持 OpenGL和 DirectX接口的显卡驱动。


2、什么是HLSL、GLSL、CG

在可编程管线出现之前,为了编写着色器代码,开发者们学习汇编语言。为了给开发者们打开更方便的大门,就出现了更高级的着色语言(Shading Language)。看巴塔言定专Y用丁黑马T色器的,常见的着色语言有DirectX的HLSL (High Level Shading Language)、OpenGL 的GLSL(OpenGL Shading Language)以放NVIDIA 的CO (C For Graphic)。h0、C出相对干C的高级那级(High-Level)”语言,但这种高级是相对于汇编语言来说的,而不是像C#相对于C的高级那样。这些语言会被编译成与机器无关的汇编语言,也被称为中间语言(Intermediate LanguageIL)。这些中间语言再交给显卡驱动来翻译成真正的机器语言,即 GPU可以理解的语言。

对于一个初学者来说,一个最常见的问题就是,他应该选择哪种语言?

GLSL 的优点在于它的跨平台性,它可以在Windows、Linux、Mac甚至移动平台等多种平台上工作,但这种跨平台性是由于OpenGL没有提供着色器编译器,而是由显卡驱动来完成着色器的编译工作。也就是说,只要显卡驱动支持对GLSL 的编译它就可以运行。这种做法的好处在于,由于供应商完全了解自己的硬件构造,他们知道怎样做可以发挥出最大的作用。换句话说,GLSL是依赖硬件,而非操作系统层级的。但这也意味着GLSL的编译结果将取决于硬件供应商。要知道,世界上有很多硬件供应商——NVIDIA、ATI等,他们对GLSL 的编译实现不尽相同,这可能会造成编译结果不一致的情况,因为这完全取决于供应商的做法。

而对于 HLSL,是由微软控制着色器的编译,就算使用了不同的硬件,同一个着色器的编译结果也是一样的(前提是版本相同)。但也因此支持HLSL 的平台相对比较有限,几乎完全是微软自已的产品,如Windows、Xbox 360、PS3等。这是因为在其他平台上没有可以编译HLSL 的编译器。

CG则是真正意义上的跨平台。它会根据平台的不同,编译成相应的中间语言。CG 语言的跨平台性很大原因取决于与微软的合作,这也导致CG 语言的语法和 HLSL 非常相像,CG 语言可以无缝移植成HLSL代码。但缺点是可能无法完全发挥出 OpenGL 的最新特性。

对于Unity平台,我们同样可以选择使用哪种语言。在Unity Shader中,我们可以选择使用“CG/HLSL"或者“GLSL”。带引号是因为Unity里的这些着色语言并不是真正意义上的对应的着色语言,尽管它们的语法几乎一样。以 Unity CG为例,你有时会发现有些CG语法在 Unity Shader中是不支持的。关于Unity Shader和真正的CG/HLSL、GLSL 之间的关系我们会在3.6节中讲到。


3、什么是Draw Call


4、什么是固定渲染管线

三、GPU与CPU

可以把CPU比喻成雕刻大师,虽然能做出很精美的作品,但让他一天雕刻1000件作品出来是不可能的。GPU则是玩具工厂,里面有很多流水线工人,每天生产几万件玩具也毫无压力,但要做出一件流芳百世的雕塑也是不可能的。

1、CPU与GPU的并行工作

CPU、GPU的关系可以用下图表示:


如果没有流水化线,那么CPU需要等到GPU完成上一个渲染任务才能发送渲染命令,但这种方法显然会造成效率低下。因此,我们通过使用命令缓冲区(Command Buffer) 让CPU和GPU并行工作。
命令缓冲区包含了一个命令队列, 由CPU 向其中添加命令, 而由GPU 从中读取命令,添加
和读取的过程是互相独立的。命令缓冲区使得CPU和GPU可以相互独立工作。当CPU需要渲染
一些对象时,它可以向命令缓冲区中添加命令, 而当GPU完成了上一次的渲染任务后,它就可以
从命令队列中再取出一个命令并执行它。
命令缓冲区中的命令有很多种类, 而Draw Call 是其中一种,其他命令还有改变渲染状态等
( 例如改变使用的着色器, 使用不同的纹理等)。

2、Draw Call对帧率的影响

在每次调用Draw Call之前, CPU需要向GPU发送很多内容,包括数据、状态和命令等。在这一阶段, CPU需要完成很多工作,例如检查渲染状态等。而一旦CPU完成了这些准备工作, GPU就可以开始本次的渲染。GPU的渲染能力是很强的,渲染200 个还是2 000 个三角网格通常没有什么区别,因此
渲染速度往往快千CPU提交命令的速度。如果Draw Call的数批太多, CPU就会把大献时间花费
在提交Draw Call上,造成CPU的过载。


减少Draw Call的方法之一是批处理(Batching),即把很多小的Draw Call合成一个大的Draw Call。由于我们需要在CPU的内存中合并网格,而合并的过程是需要消耗时间的。因此,批处理技术更加适合于那些静态的物体,例如不会移动的大地、石头等,对于这些静态物体我们只需要合并一次即可。


所以在开发中,有两点需要注意:

  • 避免使用大蜇很小的网格。当不可避免地需要使用很小的网格结构时,考虑是否可以合并它们。
  • 避免使用过多的材质。尽量在不同的网格之间共用同一个材质。

参考

UnityShader 第2章
贾天源:渲染管线与GPU(Shading前置知识)

第二章——渲染流水线 (yuque.com)

  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值