渲染流水线

 

一、渲染流水线

  渲染流水线的工作任务在于由一个三维场景出发、生存(或者说渲染)一张二维图像。换句话说,计算机需要从一系列的顶点数据、纹理等信息出发,把这些信息最终转换成一张人眼可以看到的图像。而这个工作通常是由CPU和GPU共同完成。
一个渲染流程分三个阶段:应用阶段(Application Stage)几何阶段(Geometry Stage)光栅化阶段(Rasterizer Stage)
  Unity Shader入门基础(一)-LMLPHP
Unity Shader入门基础(一)-LMLPHP

  1、应用阶段

  从名字我们可以看出,这个阶段是由我们的应用主导的,因此通常由CPU负责实现。换句话说,我们这些开发者具有这个阶段的绝对控制权。这一阶段中,开发者有3个主要任务:
  首先,我们需要准备好场景数据,例如摄像机的位置、视锥体、场景中包含了哪些模型、使用了那些光源等等;
  其次,为了提高渲染性能,我们往往需要做一个粗粒度剔除工作,以把那些看不见的物剔除出去,这样就不需要再移交给几何阶段进行处理;
  最后,我们需要设置好每个模型的渲染状态,这些渲染状态包括但不限于它使用的材质(漫反射颜色、高光反射颜色)、试用的纹理、使用的shader等。
  这一阶段最重要的输出是渲染所需要的几何信息,即渲染图元(Rendering Primitives)。通俗来讲,渲染图元可以是点、线、三角面等。这些渲染图元将会被传递给下一个阶段——几何阶段。

  2、几何阶段

  几何阶段用于处理所有和我们要绘制的几何相关的事情。例如,决定需要绘制的图元是什么,怎么绘制它们,在哪里绘制它们。这一阶段通常在GPU上进行。
  几何阶段负责和每个渲染图元打交道,进行逐顶点、逐多边形的操作。这个阶段可以进一步分成更小的流水线阶段。几何阶段的一个重要任务就是把顶点变换到屏幕空间中,再交给光栅器进行处理。通过对输入的渲染图元进行多步处理后,这一阶段将会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息,并传递给下一阶段。

  3、光栅化阶段

  这一阶段将会使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。这一阶段也是在GPU上运行。光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕上。它需要对上一个阶段的逐顶点数据(例如纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理。
 

二、CPU和GPU之间的通信

  渲染流水线的起点是CPU,即应用阶段。应用阶段大致可分为下面3个阶段:
  (1)把数据加载到显存中
  (2)设置渲染状态
  (3)调用DrawCall
  DrawCall是一个命令,它的发起方是CPU,就收方是GPU。这个命令仅仅会指向一个需要被渲染的图元列表,而不会再包含任何材质信息

三、GPU流水线

  当给定一个DrawCall时,GPU就会根据渲染状态(例如材质、纹理、着色器等)和所有输入的顶点数据进行计算,最终输出成屏幕上显示的那些漂亮的像素。而这个计算过程,就是GPU流水线。
Unity Shader入门基础(一)-LMLPHP
Unity Shader入门基础(一)-LMLPHP
  从图中可以看出,GPU的渲染流水线接收顶点数据作为输入。这些顶点数据是由应用阶段加载到显存中,再由DrawCall指定的。这些数据随后被传递给顶点着色器。
  顶点着色器(Vertex Shader)是完全可编程的,它通常用于实现顶点的空间变换、顶点着色等功能。
  曲面细分着色器(Tessellation Shader)是一个可选的着色器,它用于细分图元。
  几何着色器(Geometry Shader)同样是一个可选的着色器,它可以被用于执行逐图元(Per-Primitive)的着色操作,或者被用于产生更多的图元。
  裁剪(Clipping)这一阶段的目的是将那些不在摄像机视野内的顶点裁减掉,并剔除某些三角图元的面片。
  屏幕映射(Screen Mapping)这一阶段是不可配置和编程的,它负责把每个图元的坐标变换到屏幕坐标系中。
  三角形设置(Triangle Setup)三角形遍历(Triangle Traversal)阶段也都是固定函数的阶段,片元着色器(Fragment Shader)则是完全可编程的,它用于实现逐片元的着色操作。最后,逐片元操作(Per-Fragment Operation)阶段负责执行很多重要的操作,例如修改颜色、深度缓冲、进行混合等。
 

四、什么是DrawCall

  DrawCall本身的含义很简单,就是CPU调用图像编程接口,如OpenGL中的glDrawElements命令或者DirectX中的DrawIndexedPrimitive命令,以命令GPU进行渲染的操作。
  一个常见的误区是,DrawCall中造成性能问题的元凶是GPU,认为GPU上的状态切换是耗时的,其实不是的,真正“拖后腿”其实是CPU。
  问题一:CPU和GPU是如何实现并行工作的
  想让CPU和GPU可以并行工作,解决方法是使用一个命令缓冲区(Command Buffer)。
命令缓冲区包含了一个命令队列,由CU向其中添加命令,而由GPU从中读取命令,添加和读取的过程是相互独立的。命令缓冲区使得CPU和GPU可以相互独立工作,当CPU需要渲染一些对象时,它可以向命令缓冲区添加命令,而当GPU完成了上一次的渲染任务后,它就可以从命令队列中再取出一个命令并执行它。
Unity Shader入门基础(一)-LMLPHP
  命令缓冲区的命令有很多种类,而DrawCall是其中一种,其他命令还有改变渲染状态等(例如改变使用的着色器,使用不同的纹理等)。
Unity Shader入门基础(一)-LMLPHP
  问题二:为什么DrawCall多了会影响帧率
  在每次调用DrawCall之前,CPU需要向GPU发送很多内容,包括数据、状态和命令等。这一阶段,CPU需要完成很多工作,例如检查渲染状态等。而一旦CPU完成了这些准备工作,CPU就可以开始本次的渲染。GPU的渲染能力是很强的,渲染200个还是2000个三角网格通常没什么区别,因此渲染速度往往快于CPU提交命令的速度。如果DrawCall数量太多,CPU就会把大量时间花费在提交DrawCall上,造成CPU的过载。
Unity Shader入门基础(一)-LMLPHP
Unity Shader入门基础(一)-LMLPHP
  问题三:如何减少DrawCall
  减少DrawCall一个很显然的优化想法就是把很多小的DrawCall合并成一个大的DrawCall,这就是批处理的思想
Unity Shader入门基础(一)-LMLPHP
Unity Shader入门基础(一)-LMLPHP
  在游戏开发过程中,为了减少DrawCall的开销,有两点需要注意:
  (1)避免使用大量很小的网格。当不可避免地需要使用很小的网格结构时,考虑是否可以合并。
  (2)避免使用过多的材质。尽量在不同的网格之间共用一个材质。
 

五、什么是shader

  我们之所以要花很大篇幅来讲述GPU的渲染流水线,是因为shader所在的阶段就是渲染流水线的一部分,更具体来说,shader就是:
  (1)GPU流水线上一些可以高度编程的阶段,而由着色器编译出来的最终代码是会在GPU上运行的(队于固定管线的渲染来说,着色器有时等同于一些特定的渲染设置);
  (2)有一些特定类型的着色器,如顶点着色器、片元着色器等;
  (3)依靠着色器我们可以控制流水线中的渲染细节,例如用顶点着色器来进行顶点变换以及传递数据,用片元着色器来进行逐像素的渲染。
 
05-11 14:44