Unity名为Shader的尚方宝剑

程序员外貌协会怎么能不会Shader

Posted by Elias on March 13, 2022

Unity名为Shader的尚方宝剑

前言

身为程序员外貌协会怎么能不会Shader

本文是《Unity Shader 入门精要》一书的读书笔记,将以随笔的方式记录Shader学习路途上的一些随心所欲的想法。

本文的大标题“第x篇、第x章”将以《Unity Shader 入门精要》的章节进行分级,

小标题“x.x”以自己的小想法分级,

编号“x.”表明是概念相关的内容,

带”Q&A”的粗体部分为设问环节。

第一篇 基础篇

第1章 欢迎来到Shader的世界

程序员的浪漫就是图形学!

第2章 渲染流水线

2.1 渲染流水线是个啥,干了个啥

1.渲染流程分为三个阶段:应用阶段、几何阶段、光栅化阶段。

2.应用阶段(CPU) => 几何阶段(GPU) => 光栅化阶段(GPU)

3.应用阶段主要任务:①准备场景数据;②粗粒度剔除工作;③设置每个模型渲染状态;

4.几何阶段主要任务:①将顶点坐标变换到屏幕空间;②多步处理渲染图元,并输出屏幕空间的二维顶点坐标、每个顶点深度值、着色等相关信息;

5.光栅化阶段主要任务:①决定每个渲染图元的哪些像素应该被绘制在屏幕上;

2.2 从渲染流水线的起点开始

2.2.1 CPU阶段(应用阶段)

渲染流水线的起点是CPU,此时处于应用阶段,应用阶段又有三个小的阶段:①把数据加载到显存;②设置渲染状态;③调用Draw Call;

Q&A 1:数据是怎么加载到显存的?

A:所有的渲染数据都是从硬盘(HHD)加载到系统内存(RAM),其中的网格、纹理的一系列数据又被加载到显卡的存储空间中,即显存(VRAM)。

Q&A 2:数据为什么要加载到显存?

A:因为显卡对显存的访问速度更快。注意,并非所有的数据都会加载到显存,只有显卡需要使用到的数据才会加载到显存;其次,大多数显卡其实没有直接对RAM进行访问的权力。

讲完数据是如何加载到显存的之后,接下来我们将继续了解渲染状态的设置。

Q&A 3:什么是渲染状态?

A:通俗的讲,渲染状态定义了场景中的网格应该怎样被渲染,需要用到哪些着色器、光源属性、材质等内容;更通俗的讲,就是在Unity里为相关物体设置了什么Shader、材质、纹理等内容。

如果为多个物体设置同样的渲染状态,那么这些物体看起来仿佛就是一个材质,比如看起来都像是玻璃制造的。

在准备好应用阶段的①②工作后,接下来CPU就会调用Draw Call这样一个渲染命令,告诉GPU哪个图元列表需要被渲染了,快去渲染它。这个Draw Call命令不会包含任何材质信息,因为这些工作都已经在前置步骤完成了,GPU只关心需要去渲染谁。

再往后,就主要是GPU的事了。

2.2.2 GPU阶段(几何阶段+光栅化阶段)

在GPU中,将完成渲染流水线的几何阶段和光栅化阶段。

CPU完成它的工作时,会把处理好的顶点数据交给GPU。

2.2.2.1几何阶段

在几何阶段,顶点数据会依次进行如下的处理:

顶点数据——>顶点着色器»曲面细分着色器»几何着色器»剪裁»屏幕映射—–>屏幕空间的顶点信息

其中加粗的内容表明该Shader必须由我们编程实现,未加粗的部分表明这些Shader是可选的。

通俗的讲,几何阶段就是对接收到的顶点数据进行一系列操作,最终使得每个点带上它计算好的颜色,出现在屏幕里预期的、正确的位置上(这个过程中顶点坐标会从模型空间转换到齐次剪裁空间,接着会从三维坐标系映射到屏幕坐标系),至此,几何阶段就完成了它的工作,并向光栅化阶段输出了屏幕坐标系下的顶点位置以及与它们相关的额外信息(深度值、法线方向、视角方向等)

2.2.2.2光栅化阶段

到了光栅化阶段,有两个最重要的目标:①计算每个图元都覆盖了哪些像素;②为这些被覆盖的像素计算它们应有的颜色;

在该阶段,屏幕空间的顶点信息会进行如下的处理:

屏幕空间的顶点信息—–>三角形设置»三角形遍历»片元着色器»逐片元操作—–>屏幕图像

6.三角形设置:计算三角网格表示数据的过程叫做三角形设置。

7.三角形遍历:找到哪些像素被三角网格覆盖的过程叫做三角形遍历。 (这个阶段也被称为扫描变换)

三角形设置会计算光栅化一个三角网格所需的信息。上一个阶段的输出都仅仅是三角网格的顶点,如果需要得知整个三角网格对像素的覆盖情况,我们还需要计算每条边上像素坐标,因此就需要在此对三角网格的表示数据进行计算。

完成三角形设置后,接下来将进行三角形遍历,检查每个像素是否被某个三角形网格覆盖了,如果被覆盖了,就会生成一个片元(包含了若干信息),这一操作是遍历性的,所以叫三角形遍历。最终会输出一个片元序列,每个片元携带了若干信息,包括但不限于屏幕坐标、深度信息以及几何阶段输出的顶点信息(法线、纹理坐标)等内容,这些内容将用于后续的颜色计算。

片元着色器将在三角形遍历完成后进行,该着色器是可选择性进行的。在这一阶段可以完成诸多渲染技术,其中最重要的技术之一是纹理采样(为了在该着色器进行纹理采样,我们通常在顶点着色器阶段输出每个顶点对应的纹理坐标,再经过光栅化阶段对三角网格的3个顶点对应的纹理坐标插值,就可以得到覆盖的片元的纹理坐标了)。

逐片元操作是渲染流水线的最后一步,该阶段有两个主要任务:①决定每个片元的可见性(这涉及到很多测试工作,比如深度测试、模板测试等)。②如果一个片元通过了所有测试,就把它的颜色值和颜色缓冲区中的颜色进行合并(Merge),也可以叫混合。

一个片元如果没有通过某个测试,那么该片元将直接被舍弃,如果它通过了所有测试,那么它就可以与颜色缓冲区的颜色进行合并。由于片元没通过测试就会被舍弃,就导致了该片元在测试工作之前的所有计算工作都白费了,一定程度上导致了资源浪费,因此可以将相关测试工作提前执行,不过这样做有利有弊:虽然一定程度上节省了资源,但是也有可能发生冲突。(还好,现代GPU会判断片元着色器中的操作是否和提前测试发生冲突,如果有冲突,就会禁用提前测试)

经过重重计算和测试后,一个过关斩将的图元终于可以显示到屏幕上了,我们的屏幕现实的就是颜色缓冲区中的颜色值。但是,一整个屏幕的图元进行光栅化是需要时间的,为了避免我们看到渲染一半的情况,GPU会采用双缓冲的策略,这意味着场景的渲染是在幕后进行的(即后置缓冲区),等到整体渲染好了,GPU就会交换后置缓冲区和前置缓冲的内容,以此保证我们看到的图像总是连续的。(这也解释了为什么有些时候硬件性能更不上渲染所需,导致画面卡在某一帧,因为后台还没有渲染好最新的一帧给你看呀!)

Q&A 4:CPU和GPU如何实现并行工作的?

A:使用了命令缓冲区。命令缓冲区包含了一个命令队列,CPU往里加命令,GPU不断从里面取命令,添加和读取过程相互独立。

Q&A 5:Draw Call多了为什么影响帧率?

A:CPU执行Draw Call前会做一系列的准备工作(向GPU发送数据、状态、命令等内容),这些工作也是耗时的,因此Draw Call越多,这一部分的耗时越多,导致整体耗时变多。这和拷贝文件时,小文件过多会影响拷贝时间是一个道理:同样10MB的文件,其中一个是单个文件,另一个是10000个文件,单个文件总是能很快拷完,另一份则需要拷很久,因为每个文件拷贝之前都会分配内存、创建各种元数据等,这些会在文件数量变大时,跟着线性变大。

Q&A 6:如何减少Draw Call呢?

A:采用批处理(Batching)方法,将很多小的Draw Call合并为一个大的Draw Call,这需要在CPU的内存中合并网格,合并也是需要时间的,因此批处理技术更加适合静态的物体(不会移动的大地、石头等),于此同时,还需要注意,使用批处理合并的网格会将使用同一种渲染状态。