Unity Shader入门精要——第2章 渲染流水线

Unity Shader入门精要读书笔记系列

第1章 欢迎来到Shader的世界
第2章 渲染流水线



前言

要学Shader,不得不先说渲染流水线。渲染是从一个三维场景到显示在2D屏幕上的过程。这个过程
就是渲染流水线或者叫渲染管线。
主要功能:给定虚拟相机、三维对象、光源等,生成二维图像。


2.1 综述

什么是渲染流水线

作者从工业流水线出发,阐述工厂生产洋娃娃的过程,从而引出渲染流水线的概念。

把一个非流水线系统分成n个流水线阶段,且每个阶段耗费时间相同的话
会使整个系统得到n倍的速度提升。

一个渲染流程分成三个阶段,每个阶段包含子流水线阶段。
在这里插入图片描述

  • 应用阶段 Application
    在CPU上执行,由应用程序驱动。开发者拥有绝对控制权。
    这一阶段主要是输出渲染图元(Rendering Primitives)到几何阶段,通常是点、线、三角面等。
    1.准备场景数据
    1)摄像机数据:位置、视椎体
    2)场景物体数据:物体变换数据(位置、旋转、缩放)、物体网格数据(顶点位置、UV贴图)。
    3)光源数据:方向光、点光源、聚光源
    2.粗粒度剔除
    把不可见的物体剔除出去。
    3.设置渲染状态
    材质(漫反射颜色、高光反射颜色)、使用的纹理、使用的Shader等。

  • 几何阶段 Geometry
    在GPU上执行,对每个渲染图元进行逐顶点、逐多边形的操作。输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等。
    这一阶段主要任务是把顶点坐标变换到屏幕空间中,然后交给光栅器进行处理。

  • 光栅化阶段 Rasterization
    在GPU上执行,根据顶点位置、深度值(z坐标)、法线方向、视角方向等产生像素,并渲染出最终的图像。
    这一阶段主要任务是决定每个渲染图元中的哪些像素应该被绘制在屏幕上。
    它需要对上一阶段得到的逐顶点数据(纹理坐标、顶点颜色等)进行插值,然后进行逐像素处理。

这一小节所述流水线是概念上的。作者紧接着介绍了硬件真正用于实现上述概念的流水线。

2.2 CPU和GPU之间的通信

由2.1节可知渲染过程是从CPU到GPU,其中CPU在应用阶段大致分三个阶段:
1)把数据加载到显存中
渲染所需的数据本来都是存在硬盘(HDD)上,例如图片、模型等。
CPU会先将数据加载到系统内存(RAM)中。变成网格、纹理等数据,这个过程十分耗时。
由于显卡对显存(VRAM)的访问速度更快,且大多数显卡对于RAM没有直接访问权利。
所以需要将网格,纹理等数据加载到显存中。随后,由CPU来设置渲染状态。
2)设置渲染状态
渲染状态定义了场景中的网格是怎样被渲染的。
包括使用哪个顶点着色器(Vertex Shader)/片元着色器 (Fragment Shader)、 光源属性、材质等。
3)调用DrawCall
CPU调用一个DrawCall指向需要被渲染的图元列表,GPU就会根据设置好的渲染状态(材质、纹理、着色器等)和所有输入的顶点数据进行下一步计算。
在这里插入图片描述

2.3 GPU流水线

GPU渲染的过程就是GPU流水线,对应几何阶段和光栅化阶段。
在这里插入图片描述
这两个阶段又被分成更小的阶段。有的阶段完全由GPU固定实现,有的阶段则具有高度可配置性或可编程性。上图中蓝色表示GPU固定实现,黄色表示不可编程但是可配置,绿色表示可编程控制

顶点着色器(Vertex Shading )

顶点着色器是可编程的,输入是来自CPU的顶点数据。
进来的每个顶点都会调用一次顶点着色器。顶点着色器本身不可以创建或者销毁任何顶点,而且无法得到顶点与顶点之间的关系。例如,我们无法得知两个顶点是否属千同一个三角网格。
但正是因为这样的相互独立性, GPU 可以利用本身的特性并行化处理每一个顶点 ,这意味着这一阶段的处理速度会很快。
顶点着色器需要完成的工作主要有:坐标变换和逐顶点光照。

坐标变换:在这个过程中可以通过改变顶点的位置,来模拟水面、布料等。
坐标变换主要任务是把顶点坐标从模型空间转换到齐次裁剪空间 。例如Unity中经典的MVP矩阵:
o. pos = mul(UNITY_MVP, v.position)
齐次裁剪空间下的顶点坐标通过透视除法后,得到归一化的设备坐标(NDC)。(投影)
具体数学实现会在第4章讲到。顶点着色器可以有不同的输出方式。最常见的输出路径是经光栅化后交给片元着色器进行处理。而在现代的 Shader Model它还可以把数据发送给曲面细分着色器器 (Tessellation Shader) 或几何着色器 (Geometry Shader)。

裁剪(Clipping)

根据图元与摄像机视野的关系,完全在视野外的剔除;完全在视野内的保留;一半在视野内,一半在视野外的则需要裁剪。
在这里插入图片描述
裁剪阶段可配置正面或背面剔除。

屏幕映射(Screen Mapping)

屏幕映射的任务是把每个图元的坐标转换到屏幕坐标系 (Screen Coordinates) 下。GPU根据屏幕分辨率
对x,y坐标进行缩放。z坐标不进行任何处理。屏幕坐标系和z坐标一起构成窗口坐标系 (Window Coordinates) 。这些值会一起被传递到光栅化阶段。
屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上哪个像素以及距离这个像素有多远。
在这里插入图片描述
这里需要注意OpenGL和DirectX之间屏幕坐标系的差异。

三角形设置(Triangle Setup)

从这一步进入光栅化阶段。光栅化阶段有两个最重要的目标,计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色。
三角形设置阶段,输入是屏幕坐标系下的顶点数据(位置、深度值z、法相方向、视角方向等)。
也就是说得到的是三角形网格每条边的两个顶点。如果要得到整个三角形网格对像素的覆盖情况,就必须计算每条边上的像素坐标。
为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式(边方程)。
这样一个计算三角网格表示数据的过程就叫做三角形设置。

三角形遍历(Triangle Traversal)

三角形遍历阶段将会检查每个像素是否被一个三角网格所覆盖。如果
被覆盖的话,就会生成一个片元 (fragment) 。而这样一个找到哪些像素被三角网格覆盖的过程就
是三角形遍历,这个阶段也被称为扫描变换 (Scan Conversion)。
三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格三个顶点的顶点信息对整个覆盖区域的像素进行插值。
在这里插入图片描述
这一步输出一个片元序列。每个片元拥有屏幕坐标、深度信息、以及从几何阶段输出的顶点信息(法线、纹理坐标等)。

片元着色器(Fragment Shader)

片元着色器 (Fragment Shader) 是另一个非常重要的可编程着色器阶段。

前面的光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表述一个三角网格是怎样覆盖每个像素的。而每个片元就负责存储这样一系列数据。真正会对像素产生影响的阶段是下一个流水线阶段——逐片元操作

片元着色器的输入是上一个阶段对顶点信息插值得到的结果,更具体来说,是根据那些从顶点着色器中输出的数据插值得到的。而它的输出是一个或者多个颜色值。

这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。为了在片元着色器中进行纹理采样,通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。
在这里插入图片描述

虽然片元着色器可以完成很多重要效果,但它的局限在于,它仅可以影响单个片元。也就是说,当执行片元着色器时,它不可以将自己的任何结果直接发送给它的邻居们。有一个情况例外,就是片元着色器可以访问到导数信息 (graclien 或者说是 derivative) 。这里不多阐述。

逐片元操作 (Per-Fragment Operations)

这一阶段有几个主要任务。
1)决定每个片元的可见性。涉及很多测试工作,例如深度测试。模板测试等。
2)如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。

片元——> 模板测试——>深度测试——>混合——>颜色缓冲区

这一阶段高度可配置,可以设置每一步的操作细节。

模板测试(stencil test)

在这里插入图片描述
模板测试发生在片元着色器处理和透明度测试之后, 深度测试之前。
和深度测试类似, 模板测试也有一个对应的缓存, 即模板缓存(Stencil Buffer), 用于记录所有像素的模板值, 默认值为0。
在渲染某个物体时, 我们可以指定一个参考值(Referencce value, unity中叫Stencil ID), 当渲染某个片元时, 将片元携带的参考值与模板缓存中的值作比较, 根据不同的比较方式, 满足要求则通过测试, 然后根据设定好的操作对模板缓存中的值进行更新(+1), 当然, 也可以指定没有通过测试时的操作(舍弃片元)。

深度测试(depth test)

深度测试在透明度测试, 模板测试之后, 透明度混合之前。这一步也是高度可配置。
在这里插入图片描述
如果开启了深度测试, GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。这个比较函数也是可由开发者设置的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。通常这个比较函数是小于等于的关系,即如果这个片元的深度值大于等于当前深度缓冲区中的值,那么就会舍弃它。这是因为,我们总想只显示出离摄像机最近的物体,而那些被其他物体遮挡的就不衙要出现在屏幕上。
如果这个片元没有通过这个测试,该片元就会被舍弃。
和模板测试有些不同的是,如果一个片元没有通过深度测试 它就没有权利更改深度缓冲区中的值。而如果它通过了测试,开发者还可以指定是否要用这个片元的深度值覆盖掉原有的深度值。这是通过开启/关闭深度写入来做到的。

Unity 给出的渲染流水线中, 我们可以发现它给出的深度测试是在片元着色器之前。这种将深度测试提
前执行的技术通常也被称为Early-Z技术。 可以避免使用片元着色器计算需要舍弃的片元,提高GPU性能。

混合(blending)

如果一个片元通过了上面的所有测试,下一步就是进行混合。这一步同样是可配置的。
为什么需要混合?我们要知道,这里所讨论的渲染过程是一个物体接着一个物体画到屏幕的。而每个像素的颜色信息被存储在一个名为颜色缓冲的地方。因此,当我们执行这次渲染时颜色缓冲中往往已经有了上次渲染之后的颜色结果,那么,我们是使用这次渲染得到的颜色完全覆盖掉之前的结果,还是进行其他处理?这就是合并需要解决的问题。
对于不透明物体,开发者可以关闭混合 (Blend) 操作。这样片元着色器计算得到的颜色值就
会直接覆盖掉颜色缓冲区中的像素值。但对于半透明物体,我们就需要使用混合操作来让这个物
体看起来是透明的。
在这里插入图片描述
当模型的图元经过了上面层层计算和测试后, 就会显示到我们的屏幕上。 我们的屏幕显示的就是颜色缓冲区中的颜色值。 但是,为了避免我们看到那些正在进行光栅化的图元,GPU 会使用双重缓冲 (Double Buffering) 的策略。这意味着,对场景的渲染是在幕后发生的, 即在后置缓冲(Back Buffer) 中。 一旦场景已经被渲染到了后置缓冲中, GPU 就会交换后置缓冲区和前置缓冲(Front Buffer) 中的内容, 而前置缓冲区是之前显示在屏幕上的图像。 由此, 保证了我们看到的图像总是连续的。

2.4 补充

1)什么是OpenGL/DirectX
答:不同的图像应用编程接口。为了与GPU交互在硬件的基础上实现的一层抽象。
在这里插入图片描述

2)什么是HLSL、GLSL、CG
答:DirectX的HLSL (High Level Shading Language)、 OpenGL的GLSL(OpenGL Shading Language)以及NVIDIA的CG CC for Graphic)。 他们都是高级着色器语言,在Unity中使用的着色器语言和他们语法很相似,但不完全一样。

3)什么是Draw Call
Draw Call 本身的含义很简单,就是CPU调用图像编程接口,如 OpenGL 中的 glDrawElements 命令或者 DirectX 中的 DrawlndexedPrimitive命令,以命令 GPU 进行渲染的操作。

Draw Call造成性能问题主要在CPU端。我们先来看CPU和GPU 是如何实现井行工作的。
在这里插入图片描述
当 CPU 渲染某些对象时,它可以向命令缓冲区中添加命令。而当 GPU 完成了上 次的渲染任务后,它就可以从命令队列中再取出 个命令并执行它。这两个过程是相互独立的。
命令缓冲区中的命令有很多种类, Draw Call 是其中一种,其他命令还有改变渲染状态例如改变使用的着色器、使用不同的纹理等)。

CPU每次调用Draw Call会有一些额外的开销(比如检查渲染状态)。当调用次数过多,
因此如果Draw Call的数量太多,额外开销就会很大。CPU就会把大量时间浪费在提交DrawCall上,造成CPU的性能瓶颈。
而GPU的渲染是很快的,渲染200个还是2000个三角网格通常没有什么区别。
所有我们需要在CPU的内存中合并网格,尽可能少的提交Draw Call。
当然合并网格的过程也是很耗时的,通常情况下,需要看具体情况来平衡优化,以达到性能最优解。

猜你喜欢

转载自blog.csdn.net/qq_41044598/article/details/126342621