《Unity Shader入门精要》第二章笔记 | 渲染流水线
渲染流水线
Render-Time Rendering, Third Edition》一书中将一个渲染流程分成3个阶段:应用阶段(Application Stage) 、几何阶段(Geometry Stage) 、光栅化阶段(Rasterizer Stage) 。
应用阶段(Application Stage)
开发者有3个主要任务:
- 准备场景数据: 例如摄像机的位置、视锥体、场景中包含了哪些模型;
- 裁剪: 为了提高渲染性能,我们往往需要做一个粗粒度剔除(culling)工作,以把那些不可见的物体剔除出去,这样就不需要再移交给几何阶段进行处理;
- 设置渲染状态: 渲染状态包括但不限于它使用的材质(漫反射颜色、高光反射颜色)、使用的纹理、使用的Shader等。这一阶段最重要的输出是渲染所需的几何信息;
这一阶段最重要的输出是渲染所需的几何信息,即渲染图元。 通俗来讲,渲染图元可以是点、线、三角面等。这些渲染图元将会被传递给下一个阶段——几何阶段。
几何阶段(Geometry Stage)
几何阶段用于处理所有和我们要绘制的几何相关的事情
- 在GPU上进行
- 和每个渲染图元打交道,进行逐顶点、逐多边形的操作
- 把顶点坐标变换到屏幕空间中,再交给光栅器进行处理
- 可分成更小的流水阶段
这一阶段将会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息,并传递给下一个阶段
光栅化阶段(Rasterizer Stage)
这一阶段将会使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像
- 在GPU上进行
- 决定每个渲染图元中的哪些像素应该被绘制在屏幕上
- 需要对上一个阶段得到的逐顶点数据(例如纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理
- 可分成更小的流水阶段
CPU和GPU的通信
渲染流水线的起点是CPU,即应用阶段。应用阶段大致可分为下面3个阶段:
- 把数据加载到显存中。
- 设置渲染状态。
- 调用Draw Call
把数据加载到显存中
- 渲染所需的数据都需要从硬盘(Hard Disk Drive,HDD)中加载到系统内存
- 网格和纹理等数据又被加载到显卡上的存储空间——显存
显卡对于显存的访问速度更快,而且大多数显卡对于RAM没有直接的访问权利
设置渲染状态
- 这些状态定义了场景中的网格是怎样被渲染的。例如,使用哪个顶点着色器(Vertex Shader)/片元着色器(Fragment Shader)、光源属性、材质等
在准备好上述所有工作后,CPU就需要调用一个渲染命令来告诉GPU:“嘿!老兄,我都帮你把数据准备好啦,你可以按照我的设置来开始渲染啦!”而这个渲染命令就是Draw Call。
调用Draw Call
- Draw Call就是一个命令,它的发起方是CPU,接收方是GPU
- 这个命令仅仅会指向一个需要被渲染的图元(primitives)列表,而不会再包含任何材质信息
当给定了一个Draw Call时,GPU就会根据渲染状态(例如材质、纹理、着色器等)和所有输入的顶点数据来进行计算,最终输出成屏幕上显示的那些漂亮的像素。而这个计算过程,就是我们下一节要讲的GPU流水线。
GPU流水线
GPU渲染的过程就是GPU流水线
对于概念阶段的后两个阶段,即几何阶段和光栅化阶段,开发者无法拥有绝对的控制权,其实现的载体是GPU
几何阶段和光栅化阶段可以分成若干更小的流水线阶段,每个阶段GPU提供了不同的可配置性或可编程性。流程图如下 :
几何阶段
- 顶点着色器(Vertex Shader): 作为输入数据。在应用阶段加载到显存的。完全可编程的,它通常用于实现顶点的空间变换、顶点着色等功能。
- 曲面细分着色器(Tessellation Shader): 是一个可选的着色器,它用于细分图元。
- 几何着色器(Geometry Shader): 同样是一个可选的着色器,它可以被用于执行逐图元(Per-Primitive)的着色操作,或者被用于产生更多的图元。
- 裁剪(Clipping) ,这一阶段的目的是将那些不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片。这个阶段是可配置的。
- 屏幕映射(Screen Mapping): 这一阶段是不可配置和编程的,它负责把每个图元的坐标转换到屏幕坐标系中。
光栅化阶段
光栅化阶段有两个最重要的目标:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色
- 三角形设置(Triangle Setup): 和三角形遍历(Triangle Traversal) 阶段也都是固定函数
- 片元着色器(Fragment Shader): 则是完全可编程的,它用于实现逐片元(Per-Fragment)的着色操作
- 逐片元操作(Per-Fragment Operations): 阶段负责执行很多重要的操作,例如修改颜色、深度缓冲、进行混合等,它不是可编程的,但具有很高的可配置性
顶点着色器 [几何阶段]
顶点着色器的几个特性
- 顶点着色器的处理单位是顶点,也就是说,输入进来的每个顶点都会调用一次顶点着色器
- 顶点着色器本身不可以创建或者销毁任何顶点
- 不知道顶点和顶点之间的关系,因为这样的特性,GPU处理速度很快。
主要完成的工作:
- 坐标变换: 对顶点的坐标(即位置)进行某种变换,一个最基本的顶点着色器必须完成的一个工作是,把顶点坐标从模型空间转换到齐次裁剪空间
- 逐顶点光照
- 输出后续阶段所需的数据
裁剪 [几何阶段]
- 裁剪主要做的工作是裁剪掉那些不在摄像机视野范围的物体。
- 这一步不可被编程, 可自定义一些配置。
屏幕映射 [几何阶段]
- 屏幕映射(Screen Mapping) 的任务是把每个图元的x 和y 坐标转换到屏幕坐标系
- 屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。
- 屏幕坐标系和z 坐标一起构成了一个坐标系,叫做窗口坐标系(Window Coordinates) 。这些值会一起被传递到光栅化阶段。
注意: OpenGL把屏幕的左下角当成最小的窗口坐标值,而DirectX则定义了屏幕的左上角为最小的窗口坐标值。 如下图:
产生这种差异的原因是,微软的窗口都使用了这样的坐标系统,因为这和我们的阅读方式是一致的:从左到右、从上到下,
三角形设置 [光栅化阶段]
这个阶段会计算光栅化一个三角网格所需的信息
- 上一个阶段输出的都是三角网格的顶点,即我们得到的是三角网格每条边的两个端点
- 如果要得到整个三角网格对像素的覆盖情况,我们就必须计算每条边上的像素坐标
- 一个计算三角网格表示数据的过程就叫做三角形设置
三角形遍历 [光栅化阶段]
这个阶段将会检查每个像素是否被一个三角网格所覆盖
- 如果被覆盖的话,就会生成一个片元(fragment) 。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换(Scan Conversion)。
- 三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格3个顶点的顶点信息对整个覆盖区域的像素进行插值
- 三角形遍历的过程。根据几何阶段输出的顶点信息,最终得到该三角网格覆盖的像素位置。对应像素会生成一个片元,而片元中的状态是对3个顶点的信息进行插值得到的。
片元:
- 这一步的输出就是得到一个片元序列
- 一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。
- 这些状态包括了(但不限于)它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点信息,例如法线、纹理坐标等。
片元着色器 [光栅化阶段]
片元着色器被称为像素着色器, 但片元着色器是一个更合适的名字,因为此时的片元并不是一个真正意义上的像素。
真正会对像素产生影响的阶段是下一个流水线阶段——逐片元操作。
这一阶段可以完成很多重要的渲染技术
- 纹理采样: 我们通常会在顶点着色器阶段输出每个顶点对应的纹理坐标,然后经过光栅化阶段对三角网格的3个顶点对应的纹理坐标进行插值后,就可以得到其覆盖的片元的纹理坐标了。
- 片元着色器的局限是: 它仅可以影响单个片元. 执行片元着色器时 它不可以将自己的任何结果直接发送给它的邻居们”
逐片元操作 [光栅化阶段]
逐片元操作阶段是高度可配置性,这一阶段被称为输出合并阶段(Output-Merger) 。主要任务如下:
- 决定每个片元的可见性
- 如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。
解决每个片元的可见性问题。这需要进行一系列测试, 这里有两个最基本的测试——深度测试和模板测试, 实现过程如下:
- 如果一个片元幸运地通过了模板测试,那么它会进行下一个测试——深度测试(Depth Test)
混合操作
- 对于不透明物体,开发者可以关闭混合(Blend) 操作。
- 对于半透明物体,我们就需要使用混合操作来让这个物体看起来是透明的
- 开发者可以选择开启/关闭混合操作。
一些容易困惑的地方
什么是OpenGL/DirectX
- OpenGL和DirectX就是这些图像应用编程接口,这些接口用于渲染二维或三维图形。
- 一个应用程序向这些接口发送渲染命令,而这些接口会依次向显卡驱动(Graphics Driver)发送渲染命令,这些显卡驱动是真正知道如何和GPU通信的角色, 正是它们把OpenGL或者DirectX的函数调用翻译成了GPU能够听懂的语言,同时它们也负责把纹理等数据转换成GPU所支持的格式
一个显卡除了有图像处理单元GPU外,还拥有自己的内存,这个内存通常被称为显存, 显卡驱动就是显卡的操作系统。下图展示了这样的关系。
什么是HLSL、GLSL、CG
常见的着色语言有DirectX的HLSL(High Level Shading Language)、OpenGL的GLSL(OpenGL Shading Language)以及NVIDIA的CG(C for Graphic)
- GLSL的优点在于它的跨平台性,它可以在Windows、Linux、Mac甚至移动平台等多种平台上工作,但这种跨平台性是由于OpenGL没有提供着色器编译器,而是由显卡驱动来完成着色器的编译工作。
- 对于HLSL,是由微软控制着色器的编译,就算使用了不同的硬件,同一个着色器的编译结果也是一样的。它的跨平台性较差,几乎完全是微软的产品。
- Cg则是真正意义上的跨平台。它会根据平台的不同,编译成相应的中间语言。 CG语言的语法和HLSL非常相像,Cg语言可以无缝移植成HLSL代码。但缺点是可能无法完全发挥出OpenGL的最新特性。
在Unity Shader中,我们可以选择使用“Cg/HLSL”或者“GLSL”。带引号是因为Unity里的这些着色语言并不是真正意义上的对应的着色语言
什么是Draw Call
Draw Call 是CPU调用图像编程接口, 如OpenGL中的glDrawElements命令或者DirectX中的DrawIndexedPrimitive命令,以命令GPU进行渲染的操作。
- Draw Call中造成性能问题的元凶是CPU
带着问题去理解:
- CPU和GPU是如何实现并行工作的?
- 需要让CPU和GPU可以并行工作。而解决方法就是使用一个命令缓冲区
命令缓冲区包含了一个命令队列,由CPU向其中添加命令,而由GPU从中读取命令,添加和读取的过程是互相独立的。命令缓冲区使得CPU和GPU可以相互独立工作。
- 为什么Draw Call多了会影响帧率? 在讲原因之前,先做一个实验。
- 创建10000个小文件,每个文件的大小为1kB, 然后把它们从一个文件夹复制到另一个文件夹。
- 单独创建一个文件,大小为10MB,然后把它从一个文件夹复制到另外一个文件夹。
对比这两次操作发现,虽然文件总大小是一致的,文件数量多的一方所需要花费的时间更长。原因在于每个复制动作都要很多额外的操作,例如分配内存、创建各种元数据等。
渲染过程虽然跟上面的实验不同,但是有类似的地方。
- 在每次调用Draw Call之前,CPU需要向GPU发送很多内容,包括数据、状态和命令等
- 一旦CPU完成了这些准备工作,GPU就可以开始本次的渲染
- GPU的渲染能力是很强的,渲染200个还是2 000个三角网格通常没有什么区别,因此渲染速度往往快于CPU提交命令的速度。
如果Draw Call的数量太多,CPU就会把大量时间花费在提交Draw Call上,造成CPU的过载
批处理
解决CPU过载的方法有很多,书中讨论的是批处理
- 提交大量的Draw Call造成CPU瓶颈,CPU把时间花在提交Draw Call上了,那么一个很显然优化的方法是把Draw Call 先合并再提交。
- 批处理技术更加适合于那些静态的物体,例如不会移动的大地、石头等,对于这些静态物体我们只需要合并一次即可
减少Draw Call的开销
- 避免使用大量很小的网格, 当不可避免使用小网格时,考虑是否可以合并它们。
- 避免使用过多的材质,尽量在不同的网格之间共用同一个材质。
什么是固定渲染管线
固定函数的流水线(Fixed-Function Pipeline) ,也简称为固定管线,通常是指在较旧的GPU上实现的渲染流水线。
- 这种流水线只给开发者提供一些配置操作,但开发者没有对流水线阶段的完全控制权。
- 固定渲染管线是只可配置的管线
进化到现在,看到有许多可编程的流水线阶段,比如顶点着色器、片元着色器,这些可编程的着色器阶段可以说是GPU进化最重要的贡献
- 在GPU发展的过程中,为了继续提供固定管线的接口抽象,一些显卡驱动的开发者们使用了更加通用的着色架构,即使用可编程的管线来模拟固定管线
- OpenGL 2.0在没有真正的固定管线的硬件支持下,依靠系统的可编程管线功能来模仿固定管线的处理过程
- OpenGL 3.0是最后既支持可编程管线又完全支持固定管线编程接口的版本,在OpenGL 3.2中,Core Profile就完全移除了固定管线的概念。
什么是Shader
- GPU流水线上一些可高度编程的阶段,而由着色器编译出来的最终代码是会在GPU上运行的(对于固定管线的渲染来说,着色器有时等同于一些特定的渲染设置);
- 有一些特定类型的着色器,如顶点着色器、片元着色器等;
- 依靠着色器我们可以控制流水线中的渲染细节,例如用顶点着色器来进行顶点变换以及传递数据,用片元着色器来进行逐像素的渲染。
总结
- 渲染流水线的概念几乎贯穿全文
- Shader编程其实就是在GPU流水线上一些可高度编程的阶段
参考
《Unity Shader入门精要》
--完--
- 原文作者: 留白
- 原文链接: https://zfunnily.github.io/2022/04/shaderrenderpipline/
- 更新时间:2024-04-16 01:01:05
- 本文声明:转载请标记原文作者及链接