读者可以在开源网站github(https://github.com/candycat1992/Unity_Shaders_Book)上下载本书的源代码。
第二章 渲染流水线
渲染流水线的最终目的在于生成或者说是渲染一张二维纹理,即我们在电脑屏幕上看到的所有效果,它的输入是一个虚拟摄像机,一些光源,一些 Shader 以及纹理
Shader 仅仅是渲染流水线的一个环节
一个渲染流程分成3个阶段:应用阶段(Application Stage)、几何阶段(Geometry Stage)、光栅化阶段(Rasterizer Stage)。
应用阶段
由应用主导,通常由CPU负责
这个阶段主要任务是:对渲染数据进行粗颗粒剔除,向下阶段输出渲染图元,并设置渲染状态(主要是设置着色器,设置测试开关和参数)
视锥体:是指场景中摄像机的可见的一个锥体范围。它有上下左右近远,共6个面组成。在视锥体内的景物可见,反之则不可见。为了提高性能,只对其中与视锥体有交集的对象进行绘制
CPU可以做粗粒度剔除(culling),包括视锥体剔除(Frustum Culling)和遮挡剔除( Occlusion Culling)两种,这样可以剔除不可见的物体,减少无效数据,移交给GPU的数据更精简。CPU做的粗粒度剔除是物体层面的。
漫反射,是投射在粗糙表面上的光向各个方向反射的现象。当一束平行的入射光线射到粗糙的表面时,表面会把光线向着四面八方反射,所以入射线虽然互相平行,由于各点的法线方向不一致,造成反射光线向不同的方向无规则地反射,这种反射称之为“漫反射”或“漫射”,这种反射的光称为漫射光。很多物体,如植物、墙壁、衣服等,其表面粗看起来似乎是平滑,但用放大镜仔细观察,就会看到其表面是凹凸不平的,所以本来是平行的太阳光被这些表面反射后,弥漫地射向不同方向。漫反射的每条光线均遵循反射定律。
几何阶段
这一阶段通常在GPU上进行
几何阶段的一个重要任务就是把顶点坐标变换到屏幕空间中,再交给光栅器进行处理。通过对输入的渲染图元进行多步处理后,这一阶段将会输出屏幕空间的二维顶点坐标、每个顶点对应的深度值、着色等相关信息,并传递给下一个阶段。
由顶点着色器处理,主要是平移,旋转和缩放三种坐标变换,通过矩阵运算实现。
在几何阶段执行的Shader叫顶点着色器,在光栅化阶段执行的Shader叫片元着色器。
光栅化阶段
这一阶段将会使用上个阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。这一阶段也是在GPU上运行。光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕上。它需要对上一个阶段得到的逐顶点数据(例如纹理坐标、顶点颜色等)进行插值,然后再进行逐像素处理。
CPU 与 GPU 之间的通信
渲染流水线的起点是CPU,即应用阶段。应用阶段大致可分为下面3个阶段:
(1)把数据加载到显存中。(2)设置渲染状态。(3)调用Draw Call
把数据加载到显存中
所有渲染所需的数据都需要从硬盘(Hard Disk Drive, HDD)中加载到系统内存(Random Access Memory, RAM)中。然后,网格和纹理等数据又被加载到显卡上的存储空间——显存(Video Random Access Memory, VRAM)中
显卡对于显存的访问速度更快,而且大多数显卡对于RAM没有直接的访问权利。
开发者还需要通过CPU来设置渲染状态,从而“指导”GPU如何进行渲染工作。
设置渲染状态
什么是渲染状态呢?一个通俗的解释就是,这些状态定义了场景中的网格是怎样被渲染的。
调用 Draw Call
Draw Call就是一个命令,它的发起方是CPU,接收方是GPU。这个命令仅仅会指向一个需要被渲染的图元(primitives)列表
当给定了一个Draw Call时,GPU就会根据渲染状态(例如材质、纹理、着色器等)和所有输入的顶点数据来进行计算,最终输出成屏幕上显示的那些漂亮的像素
GPU 流水线
GPU渲染的过程就是GPU流水线。
几何阶段:顶点着色器(必须编程) - 曲面细分着色器(可选可编程) - 几何着色器(可选可编程) - 裁剪(可配置)- 屏幕映射(不能控制)
光栅化阶段:三角形设置(不能控制) - 三角形遍历(不能控制) - 片元着色器(可选可编程) - 逐片元操作(可配置)
顶点着色器(Vertex Shader)是完全可编程的,它通常用于实现顶点的空间变换、顶点着色等功能。曲面细分着色器(Tessellation Shader)是一个可选的着色器,它用于细分图元。几何着色器(Geometry Shader)同样是一个可选的着色器,它可以被用于执行逐图元(Per-Primitive)的着色操作,或者被用于产生更多的图元。下一个流水线阶段是裁剪(Clipping),这一阶段的目的是将那些不在摄像机视野内的顶点裁剪掉,并剔除某些三角图元的面片。这个阶段是可配置的。例如,我们可以使用自定义的裁剪平面来配置裁剪区域,也可以通过指令控制裁剪三角图元的正面还是背面。几何概念阶段的最后一个流水线阶段是屏幕映射(Screen Mapping)。这一阶段是不可配置和编程的,它负责把每个图元的坐标转换到屏幕坐标系中。
光栅化概念阶段中的三角形设置(Triangle Setup)和三角形遍历(Triangle Traversal)阶段也都是固定函数(Fixed-Function)的阶段。接下来的片元着色器(Fragment Shader),则是完全可编程的,它用于实现逐片元(Per-Fragment)的着色操作。最后,逐片元操作(Per-Fragment Operations)阶段负责执行很多重要的操作,例如修改颜色、深度缓冲、进行混合等,它不是可编程的,但具有很高的可配置性。
顶点着色器
顶点着色器(Vertex Shader)是流水线的第一个阶段,它的输入来自于CPU。顶点着色器的处理单位是顶点,也就是说,输入进来的每个顶点都会调用一次顶点着色器。顶点着色器本身不可以创建或者销毁任何顶点,而且无法得到顶点与顶点之间的关系。
顶点着色器需要完成的工作主要有:坐标变换和逐顶点光照
我们可以通过改变顶点位置来模拟水面,布料等
一个最基本的顶点着色器必须完成的一个工作是,把顶点坐标从模型空间转换到齐次裁剪空间
裁剪
一个图元和摄像机视野的关系有3种:完全在视野内、部分在视野内、完全在视野外。
图元——图形元素,可以编辑的最小图形单位。图元是图形软件用于操作和组织画面的最基本的素材。
例如,一条线段的一个顶点在视野内,而另一个顶点不在视野内,那么在视野外部的顶点应该使用一个新的顶点来代替,这个新的顶点位于这条线段和视野边界的交点处。
无法通过编程来控制裁剪的过程,而是硬件上的固定操作
屏幕映射
屏幕映射(ScreenMapping)的任务是把每个图元的x和y坐标转换到屏幕坐标系(Screen Coordinates)下。屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。
屏幕坐标系和z坐标一起构成了一个坐标系,叫做窗口坐标系(Window Coordinates)
三角形设置
光栅化阶段有两个最重要的目标:计算每个图元覆盖了哪些像素,以及为这些像素计算它们的颜色。
光栅化的第一个流水线阶段是三角形设置(Triangle Setup),这个阶段会计算光栅化一个三角网格所需的信息
三角形遍历
三角形遍历(Triangle Traversal)阶段将会检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元(fragment)。而这样一个找到哪些像素被三角网格覆盖的过程就是三角形遍历,这个阶段也被称为扫描变换(Scan Conversion)。
一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了(但不限于)它的屏幕坐标、深度信息,以及其他从几何阶段输出的顶点信息,例如法线、纹理坐标等。
片元着色器
。真正会对像素产生影响的阶段是下一个流水线阶段——逐片元操作(Per-Fragment Operations)。
片元着色器的输入是上一个阶段对顶点信息插值得到的结果
它的局限在于,它仅可以影响单个片元
逐片元操作
逐片元操作(Per-Fragment Operations)是OpenGL中的说法,在DirectX中,这一阶段被称为输出合并阶段(Output-Merger)
这一阶段有几个主要任务。
- (1)决定每个片元的可见性。这涉及了很多测试工作,例如深度测试、模板测试等。
- (2)如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。
模板测试和深度测试的简化流程图
什么是 OpenGL/DirectX
OpenGL和DirectX就是这些图像应用编程接口,这些接口用于渲染二维或三维图形。
这些接口架起了上层应用程序和底层GPU的沟通桥梁。一个应用程序向这些接口发送渲染命令,而这些接口会依次向显卡驱动(Graphics Driver)发送渲染命令,这些显卡驱动是真正知道如何和GPU通信的角色,正是它们把OpenGL或者DirectX的函数调用翻译成了GPU能够听懂的语言,同时它们也负责把纹理等数据转换成GPU所支持的格式。一个比喻是,显卡驱动就是显卡的操作系统。图2.18显示了这样的关系
什么是HLSL、GLSL、CG
着色语言(Shading Language)。着色语言是专门用于编写着色器的,常见的着色语言有DirectX的HLSL(High Level Shading Language)、OpenGL的GLSL(OpenGL Shading Language)以及NVIDIA的CG(C for Graphic)。HLSL、GLSL、CG都是“高级(High-Level)”语言,但这种高级是相对于汇编语言来说的,而不是像C#相对于C的高级那样。这些语言会被编译成与机器无关的汇编语言,也被称为中间语言(Intermediate Language, IL)。这些中间语言再交给显卡驱动来翻译成真正的机器语言,即GPU可以理解的语言。
什么是Draw Call
Draw Call本身的含义很简单,就是CPU调用图像编程接口
Draw Call中造成性能问题的元凶是GPU,认为GPU上的状态切换是耗时的,其实不是的,真正“拖后腿”其实的是CPU。
问题一:CPU和GPU是如何实现并行工作的?
我们需要让CPU和GPU可以并行工作。而解决方法就是使用一个命令缓冲区(Command Buffer)。
命令缓冲区中的命令有很多种类,而Draw Call是其中一种,其他命令还有改变渲染状态等(例如改变使用的着色器,使用不同的纹理等)
问题二:为什么Draw Call多了会影响帧率?
GPU的渲染能力是很强的,渲染200个还是2000个三角网格通常没有什么区别,因此渲染速度往往快于CPU提交命令的速度。如果Draw Call的数量太多,CPU就会把大量时间花费在提交Draw Call上,造成CPU的过载。
问题三:如何减少Draw Call?
批处理(Batching)
在游戏开发过程中,为了减少Draw Call的开销,有两点需要注意。
- (1)避免使用大量很小的网格。当不可避免地需要使用很小的网格结构时,考虑是否可以合并它们。
- (2)避免使用过多的材质。尽量在不同的网格之间共用同一个材质。
什么是固定管线渲染
固定函数的流水线(Fixed-Function Pipeline),也简称为固定管线,通常是指在较旧的GPU上实现的渲染流水线
所以,Shader 是什么
- GPU流水线上一些可高度编程的阶段,而由着色器编译出来的最终代码是会在GPU上运行的(对于固定管线的渲染来说,着色器有时等同于一些特定的渲染设置)
- 有一些特定类型的着色器,如顶点着色器、片元着色器等;
- ·依靠着色器我们可以控制流水线中的渲染细节,例如用顶点着色器来进行顶点变换以及传递数据,用片元着色器来进行逐像素的渲染