您需要 登录 才可以下载或查看,没有账号?注册
x
1.2什么是渲染流水线注:管线有时候会被翻译成流水线来贴近实际意思,其实是同一个单词含义也差不多 简单理解:在计算机中将3D模型转换为屏幕上的图像需要经过一些列的处理步骤,这个处理步骤就是图形流水线图形渲染流水线是实时渲染的核心组件 渲染流水线的功能是通过给定虚拟相机、3D场景物体以及光源等场景要素来产生或者渲染一副2D的图像,场景中的3D物体通过管线转变为屏幕上的2D图像。渲染管线是实时渲染的重要工具,实时渲染离不开渲染管线 图形渲染流水线主要包括两个功能: 一是将物体3D坐标转变为屏幕空间2D坐标,二是为屏幕每个像素点进行着色。 解释二:
渲染流水线的工作任务在于从一个3D的场景出发,生成(或者说渲染)我们屏幕上看到的一张2D的图像。计算机从一系列的Mesh网格数据(3D场景中的模型展开后一般包括纹理贴图和在建模软件中以线框形式显示的模型文件,而这个文件就是模型的网格数据,就是我们所说的Mesh)、纹理等信息出发,把这些信息转换为一张我们人眼可以看到的图像,而这个工作通常是由计算机的CPU和GPU共同完成的。
渲染管线的一般流程
分别是:顶点数据的输入、顶点着色器、曲面细分过程、几何着色器、图元组装、裁剪剔除、光栅化、片段着色器以及混合测试 渲染管线的一个特点就是每个阶段都会把前一个阶段的输出作为该阶段的输入 例如,片段着色器会将光栅化后的片段(以及片段的数据块)作为输入进行光照计算除了图元组装和光栅化几个阶段是由硬件自动完成之外,管线的其他阶段管线都是可编程/可配置的。 其中顶点着色器、曲面细分相关着色器、几何着色器和片段着色器是可编程的阶段,而混合测试是可高度配置的阶段。管线的可编程/可配置是渲染管理的另一个特点。 因为早期的渲染管线采用的是立即渲染模式(Immediate mode,也就是固定渲染管线),不允许开发人员改变GPU渲染的方式,而核心渲染默认(Core-profile mode)允许开发人员定制化GPU的渲染方式。
1.2渲染流程 《Real-Time Rendering,Third Edition》一书中将一个渲染流程分成3个阶段 :应用阶段(Application Stage)、几何阶段(Geometry Stage)、光栅化阶段(Rasterizer Stage)注意,这里仅仅是概念性阶段,每个阶段本身通常也是一个流水线系统,即包含了子流水线阶段。图显示了3个概念阶段之间的联系。 1.应用阶段(cpu端)
从名字我们可以看出,这个阶段是由我们的应用主导的,因此通常由CPU负责实现。 换句话说,我们这些开发者具有这个阶段的绝对控制权。在这一阶段中,开发者有3个主要任务: 首先,我们需要准备好场景数据,例如摄像机的位置、视锥体、场景中包含了哪些模型、使用了哪些光源等等 其次,为了提高渲染性能,我们往往需要做一个粗粒度剔除culling工作,以把那些不可见的物体剔除出去,这样就不需要再移交给几何阶段进行处理 最后,我们需要设置好每个模型的渲染状态。这些渲染状态包括但不限于它使用的材质(漫反射颜色、高光反射颜色)、使用的纹理、使用的Shader等。 这一阶段最重要的输出是渲染所需的几何信息,即渲染图元(rendering primitives)。通俗来讲,渲染图元可以是点、线、三角面等。这些渲染图元将会被传递给下一个阶段——几何阶段。由于是由开发者主导这一阶段,因此应用阶段的流水线化是由开发者决定的。
2.几何阶段(GPU端) 几何阶段用于处理所有和我们要绘制的几何相关的事情。例如,决定需要绘制的图元是什么,怎样绘制它们,在哪里绘制它们,这一阶段通常在 GPU上进行。 几何阶段负责和每个渲染图元打交道,进行逐顶点、逐多边形的操作。这个阶段可以进一步分成更小的流水线阶段,后面会讲到。 几何阶段的一个重要任务就是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。通过对输入的渲染图元进行多步处理后,这一阶段将会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息,并传递给下一个阶段光栅化阶段
3.光栅化阶段(GPU端) 这一阶段将会使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。这一阶段也是在GPU上运行 光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕上 它需要对上一个阶段得到的逐顶点数据(例如纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理。和上一个阶段类似,光栅化阶段也可以分成更小的流水线阶段。 提示:
读者需要把上面的3个流水线阶段和我们将要讲到的GPU流水线阶段区分开来。这里的流水线均是概念流水线,是我们为了给一个渲染流程进行基本的功能划分而提出来的。下面要介绍的GPU流水线,则是硬件真正用于实现上述概念的流水线。
解释二:(恰好同事有这本书最新版翻看了下如下划分)
渲染管线并非严格这样划分,不同的教材会有不同的划分方法
《Real Time Rendering》一书将渲染管线划分为以下四个阶段:
应用程序阶段(Application)、几何处理阶段(Geometry Processing)、
光栅化(Rasterization)和像素处理阶段(PixelProcessing)
这些阶段中的每一个本身都可以是一个流水线,如几何处理阶段下方所示,或者一个阶段可以(部分)并行化,如像素处理阶段下方所示。在这个例子中,应用程序阶段是一个单一的进程,但这个阶段也可以是流水线或并行的。请注意,光栅化会在图元(例如三角形)内找到像素。
应用阶段-通常是在CPU端进行处理,包括碰撞检测、动画物理模拟以及视椎体剔除等任务,这个阶段会将数据送到渲染管线中;
几何处理阶段-主要执行顶点着色器、投影变换、裁剪和屏幕映射的功能;
光栅化阶段-和我们上面讨论的差不多,都是将图元离散化片段的过程;
像素处理阶段-包括像素着色和混合的功能。
我们可以发现,虽然管线的划分粒度不一样,但是每个阶段的具体功能其实是差不多的,原理也是一样的,并没有太大的差异。
2.2渲染流水线概览2.2.1应用阶段 Application Stage (CPU)
准备好需要被渲染的场景数据也就是哪些物体需要被渲染如:摄像机位置、视锥体、场景中包含的模型、光源等 1.加载模型:mesh(网格顶点位置被加载到模型空间)、material、Shader、Texture(硬盘dosk[.obj→内存RAM[mesh info→显存VRAM) 2.摄像机(位置 朝向 视锥体)
3.光源(位置 类型)
为了提高渲染性能,我们往往需要做一个粗粒度剔除(culling)工作,剔除的常用方式包括视锥体剔除和遮挡剔除,(视锥裁剪Frustum culling、视口剔除Clipping、背面剔除Backface、遮挡剔除Occlusion Culling)以把哪些看不见的物体剔除出去,并对需要渲染的对象进行排序这样就不需要再移交给几何阶段进行处理。
(1)数据加载到显存 所有渲染所需的数据都需要从: 硬盘(Hard Disk Drive ,HDD)中加载到系统内存(Random Access Memory,RAM) 中然后,网格和纹理等数据又被加载到显卡上的存储空间---显存(Video Random Access Memory,VRAM)中。这是因为,显卡对于显存的访问速度更快,而且大多数显卡对于 RAM 没有直接的访问权力 【渲染所需的数据(两张纹理以及3个网格)从硬盘最终加载到显存中,在渲染时,GPU可以快捷访问这些数据】
需要注意的是,真实渲染中需要加载到显存中的数据往往复杂,例如,顶点的位置信息﹑法线方向﹑顶点颜色﹑纹理坐标等 当把数据加载到显存中后,RAM 中的数据就可以移除了。但对于一些数据来说,CPU 仍然需要访问它们(例如,我们希望 CPU 可以访问网格数据来进行碰撞检测),那么我们可能就不希望这些数据被移除,因为从硬盘加载到RAM的过程是十分耗时的。 在该阶段开发者还需要通过CPU来设置渲染状态,从而“指导”Gpu如何进行渲染工作。 (2)设置渲染状态 渲染状态包括所使用的着色器shader、纹理、材质(漫反射颜色,高光反射颜色)等。 什么是渲染状态呢?一个通俗的解释就是,这些状态定义了场景中的网格是怎样被渲染的。例如,使用哪个顶点着色器(Vertex Shader)/片元着色器(Fragment Shader)、光源属性、材质等。如果我们没有更改渲染状态,那么所有的网格都将使用同一种渲染状态。 【图在同一状态下渲染3个网格,由于没有更改渲染状态 因此3个网格的外观看起来像是同一种材质的物体】 延伸阅读
什么是前向渲染,什么是延迟渲染? 前向渲染和延迟渲染是两种光照渲染模式 早些时候,我们被显卡的功能限制,不能去改变每个像素的绘制方式,除了发送一些不同的纹理外,不能去修改顶点的数据。现在时代已经改变,我们能够基于显卡的图形管线进行编程。我们能够发送代码到显卡去改变像素的外观(颜色),使用法线纹理(normal maps)改变它们外观使其变的突起,也可以添加反射(以及大量的现实主义)。 此代码采用几何,顶点和片段着色器的形式,从本质上来说,它们控制显卡如何去渲染对象。 前向渲染: 正向渲染是大多数引擎使用的标准,即用型的渲染技术。我们为图形卡提供几何图形,将其投影并将其分解为顶点,然后将其转换并拆分为片段或像素,这些片段或像素将在传递到屏幕之前得到最终的渲染处理。 它是相当线性的,并且每次将每个几何图形通过管道一次以生成最终图像。
延时渲染: 顾名思义,在延迟渲染中,渲染将延迟一点,直到所有几何图形都通过管道为止。然后通过在底端应用着色器来生成最终图像。延迟光照是对延迟渲染的一种修改,它通过在场景中使用更多的pass来减小G缓冲区的大小。
Lighting Performance(光照性能) 标准前向渲染(Forward Rendering)光照的性能消耗也是为什么要另辟蹊径选择其他渲染方式的主要原因。在标准前向渲染(Forward Rendering)管线流程中,每个灯光都会在每个顶点/或片元上执行光照计算,这也就是常说的逐顶点光照和逐片元/像素光照。
如果你在场景中有100个几何对象,并且每个几何对象有1000个顶点,你大约就有100000多变形(非常粗略的计算)。显卡还能够很轻松的处理,但是当这些多边形被发送到片元着色器时, 昂贵的对灯光消耗会使性能急剧下降。开发者可以尝试放置光照计算到顶点着色器减少片元着色器对光照的计算。
不管它是不是此像素上最顶层的片元,还是被遮挡的片元,昂贵的光照计算都会在每个多边形的每个可见片元上执行。如果屏幕的分辨率是1024x768,你有将近800000个像素需要被渲染。你能很轻易的就达到每帧百万级的片元操作。并且大多数的片元还会被剔除(深度测试阶段),那么对于此片元的光照就算就白费了。
如果你要对这样一个达到百万级片元的场景的每一灯光进行渲染,那么你在每一帧将跃升的一个灯光数量x1000000个片元的操作上!想象一下你有一个小镇的街道上面布满点光源!
计算前向渲染(Forward Rendering)复杂度的公式参见:big O notatio,复杂度公式:O(num_geometry_fragments * num_lights)。你能看到这里的复杂度是和几何对象数量和灯光数量直接相关的。
片元是一个最终可能在屏幕上成为像素的一个”待转像素“,如果在深度测试阶段不被剔除的话,它将在屏幕上成为屏幕的最终像素。现在一些引擎通过其他的方式优化了光照计算,比如:剔除非常远的灯光,组合灯管或使用 Light maps(非常流行的,但是只能是静态的物体)。如果你有大量的灯光需要动态光照的话,我们需要一个更好的解决方案。
Deferred Rendering to the Rescue(前向渲染的救星–延迟渲染) 延迟渲染(Deferred Rendering)是一个减少光照着色对象数量有趣的方法。尤其是对于总的片元对象来说,执行光照的片元数量直接由屏幕的分辨率决定。
延迟渲染(Deferred Rendering)的复杂性,在big O notation中是O(screen_resolution * num_lights)。
现在你能明白了,你有多少的光照数量是由你对灯光数量的使用来决定的。所以你能很高兴的增加你的灯光数量。(这不意味着你可以有无限的几何对象,它们还是要经过管线的及其他处理才能到G-Buffer中。)
The Guts of Deferred Rendering(延迟渲染的细节) 每个几何对象被渲染,但是没有使用光照,使用多目标渲染(multiple render targets),绘制出多个屏幕空间大小的Buffer。深度,法线和颜色分别写入各自的buffers(图像)。然后,这些Buffers和每个灯光像的素颜色进行合成,最后生成最终的图像。 颜色,深度和正常缓冲区。
最终照明(阴影)使用三个缓冲区生成结果 (图片由astrofa,通过维基共享资源) 选择哪一个呢? 一个最简短的回答是:如果你使用了大量灯光那么你就该使用延迟渲染(Deferred Rendering)了。但是延迟渲染(Deferred Rendering)也有一些明显的缺点: • 这个处理需要显卡支持多目标渲染,老的显卡是不支持的,所有不能在上面工作,对于这个是没有变通的方案的,除非强制要求客服换显卡 • 它需要高带宽的显卡,你要发送大的Buffer数据,老的显卡可能处理不了。对于这个也没有变通的方案的,除非强制要求客服换显卡。 • 你不能使用透明对象(除非你联合 使用deferred rendering 和Forward Rendering ) • 没有抗锯齿 • 仅有一个类型的材质被允许,除非你使用了被叫做Deferred Lighting的延迟渲染修改 • 阴影依赖于光照的数量,延迟渲染没有解决任何阴影的问题。 如果你没有大量的灯光或者你想能够在比较老的显卡上允许,你应该选择使用前向渲染(Forward Rendering)并且替换你的灯光使用静态光照贴图。这个结果看起来还是令人吃惊的
延迟渲染的改进 针对延迟渲染上述提到的缺点,下面简单列举一些降低 Deferred Rendering 存取带宽的改进方案。 最简单也是最容易想到的就是将存取的 G-Buffer 数据结构最小化,这也就衍生出了 Light Pre-Pass(即Deferred Lighting) 方法。 另一种方式是将多个光照组成一组,然后一起处理,这种方法衍生了 Tile-Based Deferred Rendering。现在主流的Deferred Rendering的改进是:分块延迟渲染 Tile-BasedDeferred Rendering
分块延迟渲染 Tile-BasedDeferred Rendering 作为传统Defferred Rendering的另一种主要改进,分块延迟渲染(Tile-Based Deferred Rendering,TBDR)旨在合理分摊开销(amortize overhead),自SIGGRAPH 2010上提出以来逐渐为业界所了解。
实验数据表明TBDR在大量光源存在的情况下明显优于上文提到的Light Pre-Pass
我们知道,延迟渲染的瓶颈在于读写 G-buffer,在大量光源下,具体瓶颈将位于每个光源对 G-buffer的读取及与颜色缓冲区(color buffer)混合。这里的问题是,每个光源,即使它们的影响范围在屏幕空间上有重疉,因为每个光源是在不同的绘制中进行,所以会重复读取G-buffer中相同位置的数据,计算后以相加混合方式写入颜色缓冲。光源越多,内存带宽用量越大。而分块延迟渲染的主要思想则是把屏幕分拆成一个个小的分块,例如每 32 × 32 像素作为一个分块(tile)。计算每个分块的深度范围(depth range),求得每个 tile 的 包围盒bounding box。然后,计算每个分块的包围盒bounding box会受到哪些光源影响,把那些光源的索引储存在分块的光源列表里。最后,逐个分块进行着色,对每像素读取 G-buffer 和光源列表及相关的光源信息。因此,G-buffer的数据只会被读取1次且仅1次,写入 color buffer也是1次且仅1次,大幅降低内存带宽用量。不过,这种方法需要计算光源会影响哪些分块,这个计算又称为光源剔除(light culling),可以在 CPU 或 GPU(通常以 compute shader 实现)中进行。用GPU计算的好处是,GPU 计算这类工作比 CPU 更快,也减少 CPU/GPU 数据传输。而且,可以计算每个分块的深度范围(depth range),作更有效的剔除。
对比 Deferred Rendering,之前是对每个光源求取其作用区域 light volume,然后决定其作用的的 pixel,也就是说每个光源要求取一次。而使用 TBDR,只要遍历每个 pixel,让其所属 tile 与光线求交,来计算作用其上的 light,并利用 G-Buffer 进行 Shading。一方面这样做减少 了所需考虑的光源个数,另一方面与传统的 Deferred Rendering 相比,减少了存取的带宽。
延迟渲染把参数保存了下来,没有像前向渲染那样边运行片元着色器边进行输出合并,而是先完成完整的深度检测,再运行片元着色器,对于每个像素只进行一次光照计算就实现了效果,大大节约了光照计算复杂度。光源越多、计算越复杂,节省下的性能就越明显。
然而,延迟渲染只能给屏幕上的每一个点保存一份光照数据,所以如果这些三角形都是半透明的,延迟渲染就不能体现出半透明的细节。换句话来说,延迟渲染完全不支持Blend同理,延迟渲染也不能实现多重采样抗锯齿的功能
G-Buffer,全称Geometric Buffer ,译作几何缓冲区,它主要用于存储每个像素对应的位置(Position),法线(Normal),漫反射颜色(Diffuse Color)以及其他有用材质参数。根据这些信息,就可以在像空间(二维空间)中对每个像素进行光照处理
一般的G-Buffer精度为64位,旧的分配方式是分别使用16位浮点数储存Normal.x、Normal.y、深度信息和漫反射颜色(十六位图)。一种新的分配模式是去掉深度,同时使用8位浮点数分别储存Normal.x、Normal.y、漫反射颜色、高光颜色,再使用24位储存RGB色彩,这样还留下了一个空闲的8位通道用作机动,并且色彩精度也提升了。新分配模式的问题是normal位数下降了很多必须通过片元着色器来代行平滑。
新的支持延迟渲染的显卡可能提供超过64位的精度,可以使延迟渲染的效果更上一层楼
(3)调用Draw Call命令 接触过渲染优化应该都听说过 Draw Call。实际上,Draw Call就是一个命令,它的发起方是CPU,接收方是GPU。这个命令仅仅会指向一个需要被渲染的图元(primitives)列表,而不会再包含任何材质信息——这是因为我们已经在上一个阶段中完成了!下图形象化地阐释了这个过程。
【CPU通过调用Draw Call来告诉GPU开始进行一个渲染过程一个Draw Call会指向本次调用需要渲染的图元列表】 当给定了一个Draw Call时,GPU就会根据渲染状态(例如材质,纹理,着色器等)和所有输入的顶点数据来进行计算,最终输出成屏幕上的像素。而这个计算过程,就是GPU的流水线(本节为CPU的流水线、下一节GPU流水线) 延伸阅读
1.什么是Draw Call 理解一 Draw Call本身的含义很简单,就是CPU调用图像编程接口,如 OpenGL 中 的gIDrawElements 命令或者DirectX 中的 DrawIndexedPrimitive命令,以命令GPU进行渲染的操作。一个常见的误区是,Draw Call 中造成性能问题的元凶是GPU,认为GPU上的状态切换是耗时的,其实不是的,真正“拖后腿”其实的是CPU
理解二在Unity中,每次引擎准备数据并通知GPU的过程称为一次Draw Call,这一过程是逐个物体进行的,对于每个物体,不止GPU的渲染,引擎重新设置材质/Shader也是—项非常耗时的操作
Draw Call Batching技术,主要目标就是在一次Draw Call中批量处理多个物体。只要物体的变换和材质相同,GPU就可以按完全相同的方式进行处理。
Draw Call Batching技术的核心就是在可见性测试之后,检查所有要绘制的物体的材质,把相同材质的分为一组(一个Batch),然后把它们组合成一个物体(统一变换),这样就可以在一个Draw Call中处理多个物体了(实际上是组合后的一个物体)。但缺点是,相当于创建了一个超大物体,与此同时就需要分配相应大小的内存。这不仅会消耗更多内存,还需要消耗CPU时间。特别是对于移动的物体,每一帧都得重新进行组合,但对于静止不动的物体来说,只需要进行一次组合,之后就可以一直使用,效率要高得多。这就需要进行—些权衡,否则得不偿失。
在现代cpu上,一味地减少drawcall并不一定可以让帧数变高,而且一般移动设备上才更需要注意drawcall。
2.为什么Draw Call多了影响帧率
我们先来做一个实验:请创建10 000个小文件,每个文件的大小为1KB,然后把它们从一个文件夹复制到另一个文件夹。你会发现,尽管这些文件的空间总和不超过10MB,但要花费很长时间。现在,我们再来创建一个单独的文件,它的大小是10MB,然后也把它从一个文件夹复制到另一个文件夹。而这次复制的时间却少很多!这是为什么呢?明明它们所包含的内容大小是一样的。原因在于,每一个复制动作需要很多额外的操作,例如分配内存、创建各种元数据等。如你所见,这些操作将造成很多额外的性能开销,如果我们复制了很多小文件,那么这个开销将会很大。
渲染的过程虽然和上面的实验有很大不同,但从感性角度上是很类似的。在每次调用DrawCall 之前,CPU需要向GPU 发送很多内容,包括数据、状态和命令等。在这一阶段,CPU需要完成很多工作具体是:
- 准备渲染对象,然后将渲染对象从硬盘加载到内存,然后从内存加载到显存,进而方便GPU高速处理
- 设置每个对象的渲染状态,也就是设置对象的材质、纹理、着色器等
- 输出渲染图元,然后向GPU发送DrawCall命令,并将渲染图元传递给GPU
而一旦CPU完成了这些准备工作,GPU就可以开始本次的渲染。GPU的渲染能力是很强的,渲染200个还是2000个三角网格通常没有什么区别,因此渲染速度往往快于CPU提交命令的速度。如果Draw Call 的数量太多,CPU就会把大量时间花费在提交Draw Call 上,造成CPU的过载。图显示了这样一个例子
【命令缓冲区中的虚线方框表示GPU已经完成的命令。此时,命令缓冲区没有可以执行的命令了,GPU处于空闲状态,而CPU还没有准备好下一个渲染命令】
3.如何优化Draw Call (后续单独出篇幅这块还有很多内容)
尽管减少Draw Call的方法有很多,我们这里讨论使用批处理(Batching)的方法
提交大量很小的Draw Call会造成CPU的性能瓶颈,即CPU把时间都花费在准备Draw Call的工作上了。那么,一个很显然的优化想法就是把很多小的Draw Call合并成一个大的Draw Call,这就是批处理的思想。
需要注意的是,由于我们需要在CPU的内存中合并网格,而合并的过程是需要消耗时间的。因此,批处理技术更加适合于那些静态的物体,例如不会移动的大地,石头等,对于这些静态物体我们只需要合并一次即可。我们也可以对动态物体进行批处理。但是由于这些物体是不断运动的,因此每一帧都需要重新进行合并然后再发送给GPU,这对时间和空间都会造成一定的影响。
【利用批处理,CPU在RAM把多个网格合并成一个更大的网格,再发送给GPU,然后在一个Draw Call中渲染它们。弹要注意的是,使用批处理合并的网格将会使用同一种渲染状态。也就是说,如果网格之间需要使用不同的渲染状态那么就无法使用批处理技术】
(1)避免使用大量很小的网格。当不可避免的需要使用很小的网格结构时,考虑是否可以合并它们。
(2)避免使用过多的材质。尽量在不同的网格之间共用同一个材质。
(3)合并的网格会在一次渲染任务中进行绘制,他们的渲染数据,渲染状态和shader都是一样的,因此合并的条件至少是:同材质、同贴图、同shader。最好网格顶点格式也一致。
(4)合并本身有消耗,因此尽量在编辑器下进行合并
(5)确实需要在运行时合并的,将静态的物体和动态的物体分开合并:静态的合并一次就可以,动态的只要有物体发生变换就要重新合并
大家多搜索参考资料以便得到更准确解释,文章内容学习笔记仅供参考
|