第二章 渲染流水线
概述
- Shader 即着色器,是渲染流水线里的一个环节。
- 渲染流水线的最终目的是生成或者说渲染一张二维纹理,以虚拟摄像机、若干光源、若干Shader和纹理作为输入,输出我们在电脑屏幕上看到的效果。
- 流水线系统的瓶颈在于生产速度最慢的工序生产所需的时间。
- 渲染流水线的工作任务在于由一个三维场景出发、生产或者说渲染一张二维图像,由CPU和GPU共同完成。
- RTR4中将渲染流程分为4个阶段:应用阶段,几何阶段,光栅化阶段,像素处理。
- 应用阶段:应用主导,通常由CPU负责实现,输出渲染所需的几何信息,即渲染图元,主要完成以下任务
1.准备场景数据
2.做一个粗粒度剔除工作
3.设置好每个模型的渲染状态 - 几何阶段:处理所有和我们要绘制的几何相关的事情,通常在GPU上进行。几何阶段负责和每个渲染图元打交道,进行逐顶点,逐多边形的操作。 几何阶段的一个重要的任务就是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息。
- 光栅化阶段:利用几何阶段的数据产生屏幕上的像素,并最终渲染出最终的图像。
光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕上。它需要对上一阶段得到的逐顶点数据进行插值,然后再进行逐像素处理。
计算每个图元覆盖了哪些像素,以及为这些像素计算颜色。
应用阶段
-
渲染流水线起点在CPU,即应用阶段,大致可分为下面三个阶段:
1.把数据加载到显存
2.设置渲染状态
3.调用Draw Call -
数据加载:渲染所需数据从硬盘加载到内存,网格、纹理等数据继续加载到显存中。(显卡访问显存更快,而且大部分显卡对内存没有直接访问权限),数据加载到显存后,可以移除内存中一部分不需要的数据,但是需要保留一些,防止在进行某些操作时需要再次载入这些数据。
-
设置渲染状态,定义了场景中的网格是怎样被渲染的
-
Draw Call 一个命令,由CPU发起,GPU接收,指向一个需要被渲染的图元列表
GPU流水线(GPU渲染过程)
绿色表示完全可编程控制,黄色表示可配置不可编程,蓝色表示GPU固定实现,实线表示该Shader必须实现,虚线表示可选
- 顶点着色器:完全可编程,用于实现顶点的空间变换、顶点着色等功能。
- 曲面细分着色器:可选着色器,用于细分图元。
- 几何着色器:可选着色器,用于执行逐图元的着色操作,或者被用于产生更多的图元。
- 裁剪:将那些不在摄像机视野内的顶点裁减掉,并剔除某些三角图元的面片。该阶段可配置。
- 屏幕映射:把每个图元的坐标转换到屏幕坐标系中。
- 片元着色器:实现逐片元的着色操作
- 逐片元操作:修改颜色,深度缓冲,混合等操作。
顶点着色器
- 输入来自CPU,处理单位是顶点,顶点着色器不会创建或销毁顶点,也不会知道顶点之间的关系,由于这样顶点之间处理的独立性,我们可以并行处理每一个顶点,因此该阶段处理速度较快。
- 主要工作:坐标变换、逐顶点光照、输出后续阶段所需数据
- 坐标变换:把顶点坐标从模型空间转换到齐次裁剪空间。
- 顶点着色器有不同的输出方式,最常见的是经光栅化后交给片元着色器处理。
- 归一化的设备坐标(NDC):OpenGL中z分量在[-1,1],DX在[0,1]
裁剪
- 图元和摄像机视野的关系:完全在视野内,部分在视野内,完全在视野外,裁剪处理的就是部分在视野内的图元。
- 该过程不可编程,但是可配置
屏幕映射
- 任务:把每个图元的x,y坐标转换到屏幕坐标系下。
三角形设置
- 计算光栅化一个三角网格所需的信息。上一阶段我们得到的是三角形网格每个边的两个顶点,但是如果要得到整个三角网格对像素的覆盖情况,我们就需要每条边上的像素坐标,为了计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式,这样一个计算三角形网格的表示数据的过程就是三角形设置。
三角形遍历
- 三角形遍历会检查每个像素是否被一个三角形网格覆盖,如果被覆盖的话就会生成一个片元。
- 三角形遍历会使用上一阶段的数据,判断一个三级网格覆盖了哪些像素,并使用三个顶点的信息对覆盖区域的像素进行插值。
这一步的输出就是得到一个片元序列.需要注意的是,一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色.这些状态包括了(但不限于)它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点信息,例如法线、纹理坐标等.
片元着色器
- 前面的光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表述一个三角网格是怎样覆盖每个像素的。而每个片元就负责存储这样一系列数据。真正会对像素产生影响的阶段是下一个流水线阶段——逐片元操作(Per-Fragment Operations)。
- 片元着色器的局限在于,仅影响单个片元,不可以将自己的任何结果直接发送给它的邻居们(导数信息除外)
逐片元操作
- 渲染流水线的最后一步:逐片元操作(Per-Fragment Operations)是OpenGL 中的说法,在 DirectX中,这一阶段被称为输出合并阶段(Output-Merger)。
- 这一阶段有几个主要任务。
(1)决定每个片元的可见性。这涉及了很多测试工作,例如深度测试、模板测试等。
(2)如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。
- 模板测试通常用于限制渲染的区域
- 将深度测试提前的技术叫Early-Z技术,这是为了提前舍弃一些片元,减少片元着色器的无用运算
- 现代的GPU 会判断片元着色器中的操作是否和提前测试发生冲突,如果有冲突,就会禁用提前测试。但是,这样也会造成性能上的下降,因为有更多片元需要被处理了。这也是透明度测试会导致性能下降的原因。
- 为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲(Double Buffering)的策略。这意味着,对场景的渲染是在幕后发生的,即在后置缓冲(Back Buffer)中。一旦场景已经被渲染到了后置缓冲中,GPU就会交换后置缓冲区和前置缓冲(Front Buffer)中的内容,而前置缓冲区是之前显示在屏幕上的图像。由此,保证了我们看到的图像总是连续的。
知识补充
-
显卡驱动就是显卡的操作系统,显卡驱动的两方是GPU和图像编程接口。
-
HLSL:DX的高级着色器语言,平台有限,主要是微软的产品用,编译结果不依赖与硬件
-
GLSL:OpenGL的高级着色器语言,跨平台,依赖硬件,编译结果取决于硬件厂商
-
CG:NVIDIA
-
CPU和GPU并行工作的方法
-
为什么Draw Call多了会影响帧率?
渲染速度大于CPU提交命令的速度,如果Draw Call过多,CPU会把大量时间花在提交Draw Call上 -
如何减少Draw Call?
批处理:把很多个小Draw Call合并成一个大的Draw Call
因此,批处理技术更加适合于那些静态的物体,例如不会移动的大地、石头等,对于这些静态物体我们只需要合并一次即可。当然,我们也可以对动态物体进行批处理。但是,由于这些物体是不断运动的,因此每一帧都需要重新进行合并然后再发送给GPU,这对空间和时间都会造成一定的影响。
(1)避免使用大量很小的网格。当不可避免地需要使用很小的网格结构时,考虑是否可以合并它们。
(2)避免使用过多的材质。尽量在不同的网格之间共用同一个材质。