前言

本系列博客记录学习《Unity Shader入门精要》(冯乐乐著)中的笔记和感悟。

渲染流水线

综述

渲染流水线的最终目的在于生成或者说是渲染一张二维纹理。

什么是渲染流水线

渲染流水线的任务在于:从一系列的的顶点数据,纹理等信息出发,把这些信息最终转换成一张人眼可以看到的图像。而这个工作通常是由CPU和GPU共同完成的。 渲染流程分为三个阶段:

应用阶段

主要提供渲染图元(点,线,三角面),以及渲染状态(顶点片元着色器,光源属性,材质等),我们开发者在这一阶段有绝对控制权。

几何阶段

处理所有和我们要绘制的几何相关的事情(例如应用阶段提供的渲染图元),决定需要绘制的图元,如何绘制图元,在哪里绘制图元。这一阶段在GPU进行。 一个重要任务是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。 这一阶段会输出屏幕空间的二维顶点坐标,每个顶点对应的深度值,着色等相关信息,并传递给下一个阶段。

光栅化阶段

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

CPU与GPU之间的通信

在渲染流水线的应用阶段可细分为三个阶段 1. 把数据加载到显存中 2. 设置渲染状态 3. 调用Draw Call

把数据加载到显存中

所有渲染所需数据都需要经历硬盘——系统内存——显存这一阶段。

设置渲染状态

渲染状态定义了场景中的网格是怎样被渲染的。例如,使用哪个顶点片元着色器,光源属性,材质等。如果不对渲染状态进行更改,那么所有网格都将使用同一种渲染状态。 完成以上工作后,CPU就需要调用一个渲染命令——Draw Call告诉GPU进行渲染。

调用Draw Call

Draw Call是一个命令,发起方是CPU,接收方是GPU。这个命令仅仅会指向一个需要被渲染的图元列表(不包含顶点片元着色器,光源属性,材质等信息)。 当GPU收到Draw Call,就会根据渲染状态和所有接受到的顶点数据进行计算。最终输出成屏幕上显示的像素。而这个过程,就是GPU流水线。

GPU流水线

当GPU从CPU那里得到渲染命令(Draw Call)后就会进行一系列流水线操作,最终把图元渲染到屏幕上。

概述

几何阶段

GPU的渲染流水线接收顶点数据作为输入。这些顶点数据是由应用阶段加载到显存中,再由Draw Call指定的。这些数据随后被传递给顶点着色器PS:一般是顶点->图元->片元->像素的流程 顶点着色器(Vertex Shader):完全可编程,通常用于实现顶点的空间变换,顶点着色等功能。 曲面细分着色器(Tessellation Shader):用于细分图元。 几何着色器(Geometry Shader):可用于执行逐图元(Per-Primitive)的着色操作,或者用于产生更多的图元。 接下来是裁剪(Clipping),目的是将那些不在摄像机视野内的顶点裁剪掉,并提出某些三角图元的面片。 几何阶段的最后一个阶段是屏幕映射(Screen Mapping),负责把每个图元的坐标转换到屏幕坐标系中。

光栅化阶段

光栅化阶段中的三角形设置和三角形遍历也是固定函数。 **片元着色器(Triangle Traversal)**是完全可编程的它用于实现逐片元(Per-FragmentOperations)的着色操作

顶点着色器

顶点着色器(Vertex Shader)是流水线的第一个阶段,它的输入来自CPU。顶点着色器的处理单位是顶点,也就是说,输入进来的每个顶点都会调用一次顶点着色器。 需要完成的工作有:坐标变换和逐顶点光照,也可以输出后续阶段所需的数据。

坐标变换

对顶点的坐标进行某种变换。一个最基本的顶点着色器必须完成的一个工作是:把顶点坐标从模型空间转换到齐次裁剪空间

计算顶点颜色

顶点着色器会对顶点位置进行坐标变换并计算顶点颜色。

裁剪

完全在视野内的图元就继续传递给下一个流水线阶段,完全在视野外的图元就不会继续向下传递,因为他们不需要被渲染。而那些部分在事业内的图元需要进行一个处理,这就是裁剪。

屏幕映射

任务是把每个图元的x和y坐标转换到屏幕坐标系。 注意,OpenGL把屏幕的左下角当成最小的窗口坐标值,而DirectX则定义了屏幕左上角为最小的窗口坐标值。

三角形设置

由这一步开始就进入了光栅化阶段,从上一个阶段输出的信息时屏幕坐标系下的定点位置以及和它们相关的额外信息,如深度值(z坐标),法线方向,视角方向等。 光栅化阶段有两个最重要的目标:计算每个图元覆盖了哪些像素,以及为这些像素计算他们的颜色。 三角形设置:计算三角网格表示数据,并把此数据输出给下一阶段的过程

三角形遍历

将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖就会生成一个片元。这个过程也叫做扫描变换 一个片元并不是真正的像素,还包含了很多状态,这些状态计算每个像素的最终颜色。这些状态包括屏幕坐标,深度信息,顶点信息等。

片元着色器

片元着色器输入是上阶段对顶点信息插值的结果,具体来说就是从顶点着色器输出的数据插值得到的,而他的输出是一个或多个颜色值。 前面的光栅化阶段实际上并不会影响屏幕上的每个像素的颜色值,而是会产生一系列的数据信息,用来表述一个三角网格是怎样覆盖每个像素的。而每个片元就负责储存这样一系列数据。真正会对像素产生影响的阶段是——逐片元操作

逐片元操作

这是渲染流水线的最后一步。 主要任务有 1. 决定每个片元的可见性。设计测试工作,比如深度测试,模板测试 2. 如果一个片元通过了所有测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区的颜色进行合并或者说是混合。 这个阶段需要解决每个片元的可见性问题,这需要一系列测试。

模板测试

模板测试和我们经常听到的颜色缓冲,深度缓冲几乎是一类东西。 如果开启模板测试,GPU会先读取模板缓冲区中该偏远位置的模板值,然后将该值和读取到的参考值进行比较。更加具体的规则(取舍条件)完全可以由开发者自己定制。

深度测试

如果开启深度测试,GPU会把该片元的深度值和已经存在于深度缓冲区中的深度值进行比较。更加具体的规则(取舍条件)完全可以由开发者自己定制。

混合

关闭后片元着色器计算得到的颜色值就会直接覆盖掉颜色缓冲区中的像素值。

一些专业术语科普

OpenGL/DirectX

OpenGL和DirectX是我们访问GPU的桥梁。封装了很多编程接口,一个应用程序向这些接口发送渲染命令,这些接口会依次向显卡驱动发送渲染命令,这些显卡驱动知道如何和GPU通信,正是他们把OpenGL或DirectX的函数调用翻译成了GPU能听懂的语言,同时也负责把纹理等数据转换成GPU所支持的格式。

HLSL,GLSL,CG

这三个都是编写Shader的语言。 DirectX的HLSL(High Level Shading Language),OpenGL的GLSL(OpenGL Shading Language),NVIDIA的CG(C for Grapgic). GLSL:跨平台,但由于硬件供应商不同,编译结果可能不同 HLSL:微软控制着色器的编译,就算硬件供应商不同,同一个着色器的编译结果也是一样的。 CG:真正的跨平台,自适应平台。

Draw Call

一个常见误区是Draw Call中造成性能问题的元凶是GPU,认为GPU上的状态切换是耗时的,其实不是的,真正拖后腿的是CPU。

问题:CPU和GPU如何实现并行工作

命令缓冲区(Command Buffer)

问题:为什么Draw Call多了会影响帧率

在每次调用Draw Call之前,CPU需要向GPU发送很多内容,包括数据,状态和命令等。CPU在这一过程中也需要完成很多工作,比如检查渲染状态。而一旦CPU完成了这些准备工作,GPU就可以开始本次渲染。GPU渲染能力是很强的,渲染200个还是2000个三角网格通常没什么太大区别,因此渲染速度往往快于CPU提交命令的速度。

问题:如何减少Draw Call

方法之一:批处理 提交大量很小的Draw Call会造成CPU的性能瓶颈,即CPU把时间都花费在了准备Draw Call工作上了,那么一个很显然的有话想法就是把很多Draw Call合并成一个打的Draw Call,这就是批处理的思想。 由于我们需要在CPU的内存中合并网格,而合并的过程是需要消耗时间的。因此,批处理技术更加适合那些静态的物体,例如不会移动的大地,石头等。对于这些静态物体。我们只需要合并一次即可。当然,我们也可以对动态物体进行批处理。但是由于这些物体是不断运动的,因此每一帧都需要进行合并然后发给GPU,这对空间和时间都会造成一定的影响。 正常开发中,我们需要注意两点

  • 避免使用大量很小的网格
  • 避免使用过多的材质,尽量在不同的网格之间共用一个材质。

总结

Shader就是 - GPU流水线上一些可高度编程的阶段,而由着色器编译出来的最终代码是会在GPU上运行的。 - 有一些特定类型的着色器,如顶点着色器,片元着色器等 - 依靠着色器我们可以控制流水线中的渲染细节,例如用顶点着色器来进行顶点变换以及传递数据,用片元着色器来进行逐像素的渲染。

渲染流水线流程图