渲染管线与渲染路径

渲染管线

前言

学习完GAMES101后,一直想总结一下渲染管线相关的知识。最近在看乐乐姐的Unity Shader入门精要,觉得讲的非常好,结合RTR4的书籍资料,在这里做出一份总结。
文中的插图主要来自于《Unity Shader 入门精要》与《RTR4》。
《Unity Shader 入门精要》配套插图链接:
http://candycat1992.github.io/unity_shaders_book/unity_shaders_book_images.html

流程概述

在RTR3中,渲染管线分为3个阶段:
在这里插入图片描述

而在RTR4中,渲染管线则分为4个阶段:
在这里插入图片描述
可以看到,在应用阶段、几何阶段、光栅化阶段后,还新添加了像素处理阶段。

这些阶段中的每一个阶段本身可能是一个管道,比如图中的几何阶段,或者一个阶段可能(部分)并行化,比如图中的像素处理阶段。而从图中来看,应用阶段似乎是单个进程,但是实际上这个阶段也可以是流水线或者是并行化的。

  • 应用阶段
    顾名思义,应用程序阶段由应用程序驱动,因此通常在运行于 CPU 上的软件中实现。当然一般是多核CPU去并行处理。传统上在 CPU 上执行的一些任务包括碰撞侦测、全局加速算法、动画、物理模拟等等,这取决于应用程序的类型。
    而这一阶段中开发者有三个主要任务:
    准备场景数据(摄像机、视锥体、模型、光源等)、粗粒度剔除(culling,模型层面,剔除不可见物体)、设置每个模型的渲染状态(每个模型使用的材质、纹理、shader等)。
    应用阶段最重要的输出是渲染所需的几何信息,RTR4叫做rendering primitives,中文可以翻译为渲染图元。通俗来讲,渲染图元可以是点、线、三角面等。

  • 几何阶段
    RTR4中将这一阶段划分为:顶点着色、投影、裁剪和屏幕映射。这一阶段通常在GPU上进行。
    在这里插入图片描述
    几何阶段负责和每个渲染图元打交道,进行逐顶点、逐多边形的操作。几何阶段的一个重要任务就是把顶点坐标变换到屏幕空间中,再交由光栅器进行处理。

这一阶段将会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息,并传递给下一个阶段。

需要注意的是,在顶点着色器中,我们常常对每个顶点乘上一个MVP矩阵,但其实这一步是转换到裁剪空间中,而MVP中的P矩阵projection投影矩阵虽然包含“投影”二字,但其实并没有进行真正的投影工作,而是在为投影做准备。真正的投影其实发生在后面的齐次除法中(homogeneous division)。因此经过投影矩阵的变换后顶点的w分量(齐次坐标)会具有特殊的意义(为了之后进行齐次除法)。

  • 光栅化阶段
    光栅化阶段通常将三个顶点作为输入,形成一个三角形,并找到三角形内的所有数据,如何将这些像素转发到下一个阶段。

  • 像素处理阶段
    这一阶段按每个像素执行程序以确定其颜色,并可以进行深度测试以查看其是否可见。它还可以执行逐像素操作,例如将新计算的颜色与以前的颜色混合。

在这里插入图片描述
RTR4的原图如上,光栅化阶段分为三角形设置与三角形遍历,像素处理阶段分为像素着色和合并。

可以看到,RTR4只是拆分成了两个阶段。在以前则只是把像素处理阶段并入光栅化阶段了。

光栅化阶段还需要对几何阶段传输来的一些顶点数据(纹理坐标、顶点颜色等)进行插值,如何再进行逐像素处理。

CPU与GPU之间的通信

渲染管线的起点是CPU,即应用阶段,可大致分为以下3个阶段:

1. 把数据加载到显存中

所有渲染所需的数据都需要从硬盘中加载到系统内存中。然后网格、纹理、顶点信息等等数据又被加载到显卡上的存储空间——显存(VRAM)中。
在这里插入图片描述

2. 设置渲染状态
即指定该模型用什么材质,光源属性是什么样的之类的。

所以为了合批,我们常把一些信息放在顶点信息如TEXCOORD中,而去使用同一种材质。

如下,利用批处理,CPU在RAM把多个网格合并成一个更大的网格,再发送给GPU,然后在一个Draw Call中渲染它们。因此这些网格使用的都是一种状态,所以材质必须相同。
在这里插入图片描述
而若是动态物体,由于是不断运动的,因此要合批的话每一帧都需要进行重新合并,这会有额外的空间和时间开销。

3. 调用Draw Call
Draw Call就是一个命令,发起方是CPU,接收方是GPU,由于上一阶段已经设置好了渲染状态,所以Draw Call命令只需要指向一个被渲染的图元列表,告诉GPU要渲染什么了。

而给定了一个Draw Call,GPU就会根据渲染状态(如材质、纹理、着色器等)和所有的顶点数据去进行计算。

注意,命令缓冲区的命令有很多种类,不仅有Draw Call,还有如改变渲染状态的命令:
在这里插入图片描述

UE4的官方资料

在这里插入图片描述
如上图,从左到右:硬盘、内存、CPU/GPU,CPU/GPU计算完都要将数据返回到内存。数据会在CPU和GPU之间来回传输,一部分工作在CPU上一部分在GPU上,他们之间是同步的。

线程的执行:
在这里插入图片描述

GPU流水线

在这里插入图片描述

顶点着色器(Vertex Shader)

需要强调的是,顶点着色器无法得到顶点与顶点之间的关系。例如无法知道两个顶点在模型中是否连成线。但是正是这样的相互独立性使得这一阶段具有非常好的并行性。

这一阶段的两个主要任务:一是去计算每个顶点在裁剪空间中的位置(很多时候我们就乘一个MVP矩阵转化到齐次裁剪坐标系下,接着通常再由硬件做透视除法后得到归一化的设备坐标(NDC));二是去写这个顶点需要输出的数据(比如unity shader书写中的v2f,vertex to fragment结构体),例如法线、纹理坐标(法线变换需要注意的是,所乘的矩阵为MV的逆转置,用于将法线从模型空间转换到观察空间,而非MV)。

当然也可以在顶点着色器中就去逐顶点光照计算每个顶点的颜色,然后存储起来,在后面的三角形中直接进行插值。传统上一个物体的大部分着色就是这样做的。

可选顶点处理(Optional Vertex Processing)

每一个pipeline都会有刚才所述的顶点处理,而处理完后则有一些可选阶段,当然这是GPU的流水线概述,所以这些阶段都是可以发生在GPU上的。

按照这样的顺序执行:tessella-tion, geometry shading, and stream output(曲面细分,几何着色和流输出)

它们的使用既取决于硬件的功能(并非所有 GPU 都有这些功能),也取决于程序员的愿望。它们是相互独立的,一般不常用。

  • 曲面细分着色器
    我理解中就是LOD。让一个曲面生成适当数量的三角形。比如通过ddx和ddy来进行判断。

  • 几何着色器
    书上原话说的是:The next optional stage is the geometry shader. This shader predates the tessellation shader and so is more commonly found on GPUs
    应该是说几何着色器很早就诞生出来并用于使用了,所以在GPU上更常见,但是流程上应该是在曲面细分着色器之后的。
    它有点像曲面细分着色器,因为它会使用各种不同的渲染图元去产生新的顶点。
    几何着色器有多种用途,其中最受欢迎的是生成粒子。想象一下模拟烟花爆炸。

  • 流输出
    最后一个可选的阶段称为流输出。这个阶段让我们使用 GPU 作为一个几何引擎。
    与我们把处理后的顶点数据传输到屏幕去渲染不同,这一阶段可以选择把这些顶点数据输出到一个数组中去进行更进一步的处理,而这些数据则可以在之后的pass中由CPU或者GPU使用。这个阶段通常用于粒子模拟,例如我们的焰火例子。
    (这里我也不太熟悉,不知道自己翻译的对不对)。

裁剪(Clipping)

一个图元和摄像机视野(视锥体)的关系有三种:完全在视野内、部分在视野内、完全在视野外。

完全在视野内的图元就继续传递给下一个流水线阶段,完全在视野外的图元则不会传递。因此这一步要处理(裁剪)的其实是部分在视野内的图元。

裁剪过程示意图:
在这里插入图片描述

之前说过,齐次除法后会到一个归一化的设备坐标(OpenGL中叫NDC),即变化到一个立方体内,这也是上面裁剪示意图的英文描述部分的解释:
在这里插入图片描述

并且注意上图,透视投影得到的裁剪空间的近平面为:
(x, y, z, w) = (-Near, -Near, -Near, Near) ~ (Near, Near, -Near, Near)
远平面为:
(x, y, z, w) = (-Far, -Far, -Far, Far) ~ (Far, Far, Far, Far)
其实际上是由原来的裁剪空间进行了缩放得到。这样的缩放是为了方便裁剪。
比如此时如果一个顶点在视锥体内,那么它变换后的坐标就必须满足:
-w <= x, y, z <= w
否则就不在视锥体内。

屏幕映射(Screen Mapping)

就是把每个图元的x、y坐标转换到屏幕坐标系而已。

值得一提的是每个像素的中心对应的浮点数都需要加0.5。因此一个像素范围[0, 9]其实覆盖了范围 [0.0, 10.0)
转换过程:
在这里插入图片描述
这里d是像素的离散(整数)索引,c是像素的连续(浮点)值。

三角形设置(Triangle Setup)

就是去计算一个三角网格所需的信息。因为上一阶段输出的都是三角网格的顶点信息,这一阶段就是由三角形这三个点去计算出三条边的方程(得到三角形边界的表示方式),或是一些其他为了三角形计算的数据。

三角形遍历(Triangle Traversal)

这一阶段就是根据上一阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格的3个顶点的顶点信息来对整个覆盖区域的像素进行插值。

示意图:
在这里插入图片描述

像素着色器(Pixel Shader)

或叫片元着色器。

输入是上一个阶段对顶点着色器输出的数据进行插值得到的结果。输出则是一个或多个颜色值。

这一阶段可以完成很多重要的渲染技术,比如纹理采样。

而为了在这一步进行纹理采样,通常会在顶点着色器的输出(v2f)中包含每个顶点对应的纹理坐标,这样就能在插值后得到对应片元的坐标了。

合并(Merging)

每个像素的信息存储在颜色缓冲区中(color buffer)。合并阶段的职责就是去讲像素着色器产生的像素颜色和当前存储在缓冲区的颜色结合起来。

这一阶段也叫做“ROP”,即“raster operations (pipeline)” 或 “render output unit,”

这个阶段还可以决定每个片元的可见性。

因此这一阶段主要有两个任务:

  1. 决定每个片元的可见性。涉及到很多测试工作,如深度测试、模板测试。
  2. 如果一个片元通过了所有的测试,就可以把这个片元的颜色值和颜色缓冲区的颜色进行合并(混合)。

这个阶段不是完全可编程的,但是是高度可配置的。即我们可以设置每一步的操作细节。

帧缓存

当模型的图元经过上面的层层计算和测试后,就会显示到我们的屏幕上。我们的屏幕显示的就是颜色缓冲区的颜色值。

但是为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲(Double Buffering)的策略,或者叫帧缓存更合适一些。因为现在还有三重缓冲(即加一个中缓存)的情况。

默认情况下有两个帧缓存,即Back Buffer和Front Buffer,也被我们称为前缓存和后缓存。

显卡在渲染画面的时候并不会直接交给显示器去显示,而是先写入BackBuffer也就是后缓存中,等待后缓存写入完毕,前后缓存发生交替,后缓存就变成了前缓存,前缓存就变为了后缓存。这个交替的过程被我们称为Bufferswap(帧传递)。

关于帧缓存与垂直同步,强烈推荐视频:
https://www.bilibili.com/video/BV1FK4y1x7bk?from=search&seid=3279369838866778797&spm_id_from=333.337.0.0
讲的非常棒!

渲染路径

前言

关于渲染路径,我主要参考了《大象无形》、《Unity Shader入门精要》和may老师的TA百人计划:
https://www.bilibili.com/video/BV1244y1i7oV?p=2

概述

渲染路径(Rendering Path),就是决定光照的实现方式。简言之,就是当前渲染目标使用光照的流程。

这里主要谈的是延迟渲染(Deferred Rendering)与前向渲染(Forward Rendering)。

前向渲染

流程如下图所示:
在这里插入图片描述
流程:待渲染几何体 → 顶点着色器 → 片元着色器 → 渲染目标

在渲染每一帧的时,每一个顶点/片元都要执行一次片元着色器代码,这时需要将所有的光照信息传到片元着色器中。

并且无论光源影响大不大,计算的时候都会把所有光源计算进去,这样就会造成一个很大的浪费。

关于更多的学习可以看后文写的Unity中的前向渲染。

补充

图片源自UE4实时渲染官方教程的自截图:
在这里插入图片描述

延迟渲染

我理解的延迟渲染,是为了让物体数量与光照数量解耦。主要用来解决大量光照渲染的方案。

所谓延迟,就是将”光照渲染“延迟。每次渲染都会把物体的BaseColor、粗糙度、表面法线、像素深度等分别渲染成图片,然后根据这些图再逐个计算光照。好处是无论场景中有多少物体,经过光照准备阶段后都只是变成了几张贴图,其实质是先找出来你能看到的所有像素,再去迭代光照。

流程如下图所示:
在这里插入图片描述
流程为:待渲染几何体 → 顶点着色器 → MRT → 光照计算 → 渲染目标

比如一个场景有5个物体和6个光源,每个物体都单独计算光照,那么计算量将是5 * 6 = 30次光照计算。随着光源数量提升,这样做的代价就会非常高昂。

而在延迟渲染中,我们首先将场景渲染一次,获取到的待渲染对象的各种几何信息存储到G-buffer中,然后第二个pass再遍历所有G-buffer中的位置、颜色、法线等参数,执行一次光照计算。

因此这样做,第一个pass我们把每个物体渲染成多张贴图,这里5个物体就需要5次渲染。第二个pass我们逐光源计算光照结果,这里6个光源则需要6次渲染。

于是就把之前的5 * 6 = 30次渲染计算量降低为5 + 6 = 11次渲染计算量。

G-buffer

G-Buffer,全称Geometric Buffer ,它主要用于存储每个像素对应的位置(Position),法线(Normal),漫反射颜色(Diffuse Color)以及其他有用的材质参数。

下图为一个典型的G-Buffer:
在这里插入图片描述

补充

图片源自UE4实时渲染官方教程的自截图:
在这里插入图片描述

UE4相关渲染路径

虚化引擎对于场景中所有不透明物体的渲染方式,就是延迟渲染。而对于透明物体的渲染方式,则是前向渲染。

这里搬运书本《大象无形》的插图,为虚幻引擎延迟渲染过程流程图:
在这里插入图片描述

在编辑器中,我们可以按如下设置对一个场景打开缓冲显示(这里我的UE4版本是4.26.2,场景为初学者包自带的一个场景):
在这里插入图片描述
然后就能看到所有的G-buffer,并且能随着镜头晃动而同时更新:
在这里插入图片描述
在这里插入图片描述
这里我放一张中文截图放一张英文截图,大家可以看一下G-buffer中存着什么东西。

Unity相关渲染路径

可参考官方手册:

https://docs.unity.cn/cn/current/Manual/RenderingPaths.html

我的Unity版本:2020.1.6f1c1

每个摄像机都能设置该摄像机使用的渲染路径:
在这里插入图片描述
Graphics Settings 在 Edit -> Project Settings -> Graphics中:
在这里插入图片描述
每个Pass中可以使用LightMode标签实现指定该Pass使用的渲染路径,如:
在这里插入图片描述
除了ForwardBase,LightMode还可以选Always、ForwardAdd、Deffered、ShadowCaster、PrepassBase等。

Unity中的前向渲染

Unity中,前向渲染路径有3种处理光照(即照亮物体)的方式:逐顶点处理、逐像素处理、球谐函数(Spherical Harmonics, SH)处理。而决定一个光源使用哪种处理模式取决于它的类型和渲染模式:
在这里插入图片描述
在这里插入图片描述
Unity使用的判断规则:

  1. 场景中最亮的平行光总是按逐像素处理的
  2. 渲染模式被设置成Not Important的光源,会按逐顶点或SH处理
  3. 渲染模式被设置成Not Important的光源,会按逐像素处理
  4. 如果根据以上规则得到的逐像素光源数量小于Quality Setting中的逐像素光源数量(Pixel Light Count),会有更多的光源以逐像素的方式进行渲染。

这里的Pixel Light Count(最大像素光照数量)可以在Edit -> Project Settings -> Quality中进行修改:
在这里插入图片描述

而在Pass中计算光照有Base Pass和Additional Pass:
在这里插入图片描述
对于前向渲染而言,一个Unity Shader通常会定义一个Base Pass(Base Pass也可以定义多次,例如需要双面渲染等情况)以及一个Additional Pass。一个Base Pass仅会执行一次(定义了多个Base Pass的情况除外),而一个Additional Pass会根据影响该物体的其他逐像素光源的数目被多次调用,即每个逐像素光源会执行一次Additional Pass。

Unity中的延迟渲染

使用延迟渲染时,Unity要求我们提供两个Pass:

  1. 第一个Pass用于渲染G-buffer。在这个Pass中我们会把物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的G-buffer中。对于每个物体来说这个Pass只会执行一次。
  2. 第二个Pass用于计算真正的光照模型。这个Pass会使用上一个Pass中渲染的数据来计算最终的光照颜色,再存储到帧缓冲中。

不同渲染路径的特性

1、后处理方式不同

如何需要深度信息进行后处理的话,前向渲染需要单独渲染出一张深度图,而延迟渲染就可以直接从G-buffer中的深度信息来进行计算

2、着色计算不同(shader)

由于延迟渲染光照计算统一是在LightPass中计算的,所以只能算一个光照模型(如果需要其他的光照模型,只能切换pass)

3、抗锯齿方式不同

比如目前延迟渲染由于一些硬件原因(显卡的显存、带宽等),一般实现中都不支持MSAA,而改用如TAA的方式。

渲染路径比较

Unity官方文档给了如下图:
https://docs.unity.cn/cn/current/Manual/RenderingPaths.html
在这里插入图片描述

不同渲染路径优劣

这部分总结对应may老师视频的15:38处:
https://www.bilibili.com/video/BV1244y1i7oV?p=2

前向渲染缺点

  1. 光源数量对计算复杂度影响巨大

  2. 访问深度等数据需要额外计算

前向渲染优点

  1. 支持半透明渲染

  2. 支持使用多个光照pass

  3. 支持自定义光照计算方式(延迟渲染因为是用整个Light Pass去计算所有的光照,所以不支持每一个物体用单独的光照方式计算)

延迟渲染缺点

  1. 对MSAA支持不友好

  2. 透明物体渲染存在问题

  3. 占用大量的显存带宽

延迟渲染优点

  1. 大量光照场景优势明显

  2. 只渲染可见像素,节省计算量

  3. 对后处理支持良好

  4. 用更少的shader

补充:其他渲染路径

  • 延迟光照(Light Pre-Pass / Deferred Lighting)
    减少G-buffer占用的过多开销,支持多种光照模型
    和延迟渲染的区别:用更少的Buffer信息,着色计算的时候用的是forward,所以第三步开始都是前向渲染(也就意味着可以对不同的几何物体进行不同的Shader进行渲染,所以每个物体的材质属性将有更多的变化)。

  • Forward+(即Tiled Forward Rendering,分块正向渲染)
    减少带宽,支持多光源,强制需要一个preZ
    通过分块索引的方式,以及深度和法线信息达到需要进行光照计算的片元进行光照计算。因为需要深度和法线信息所以需要单独渲染提出来。
    强制使用了一个preZ,进行了一个深度预计算。

  • 群组渲染(Clustered Rendering)
    带宽相对减少,多光源下效率提升
    其实分为Forward和Deferred两种

还有一篇知乎文章可以参考一下:https://zhuanlan.zhihu.com/p/54694743

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值