设计师的着色器第一课:渲染管线

September 27th, 2018 | Deng

这是胡罗舶团队设计师学习 Shader 计划的第一步。

在学习 Shader 之前,非常有必要对渲染管线做一个初步了解。如果你有使用游戏引擎的经验,将会帮助你学习下面内容。

渲染管线,管线我们可以理解为流水线、流程。渲染管线通俗讲就是电脑显卡将三维模型转化为二维图像显示在屏幕上的过程

渲染管线可分为“固定渲染管线”和“可编程渲染管线”。

固定渲染管线与可编程渲染管线的区别,固定渲染管线中显卡可供开发者参与修改编辑的东西极其有限,只能按照指定的规则传输数据。在早期的街机里有用到,已经成为过去式了。

现在我们谈的更多是可编程渲染管线。可编程渲染管线相对固定渲染管线,在某些步骤上可供开发者高度参与、修改和编辑。

 

Render pipelines_coolhobo
渲染管线 Render pipelines

这是一个可编程渲染管线的流程,咋一看很抽象,不用怕,我们从左边开始解读。

首先,最左边的 Buffer Object,其实就是顶点数据,这里很贴近建模美术。我们在三维软件(比如Maya)中建模,最后导出不管是 OBJ 还是 FBX 格式,里面储存的都是顶点数据,其中包括了法线、位置、颜色、UV。顶点也就是我们建模中所熟知的点线面中的“点”。除了顶点数据外还携带了一个索引数据,这个数据主要是告诉哪三个顶点构成一个面。而这些数据就已经可以完整表达你导出的三维模型是如何通过点线面构成的。

接下来到 Vertex Processor 顶点处理(着色器),它是用来处理从 Buffer Object 传输过来的顶点数据(法线、位置、颜色、UV),并对其进行加工,加工完后输出对应的顶点数据,也就是说传输过来3个顶点,对应输出也是3个顶点,不会多也不会少,在顶点着色器这个阶段只是做了进一步加工而已,如何加工,我们后面会讲到。

接下来到 Primitive Assembly 图元组装。这一步就是将顶点按照索引数据里面的规则组成三角面(3个点)或线(2个点)。这里组装成的图元不仅有面,还包括了线,点这些图形的基本元素。

Primitive assembly
图元组装

 

接下来到 Clip Project Viewport Cull 空间裁剪。这个解释需要用到下面这张图,想象一下游戏引擎里面的摄像机,它是由远近裁剪面组成的一个梯状结构,我们叫作“视锥体”。而这一步就是判断在上一步组成的这些图元是否包含在视锥体中,是的话就能看到,否则看不到。你可以在游戏引擎中架个摄像机,再放个几何体,移动几何体进出视锥体范围。就能在游戏窗口中浏览得到结果。同时,包含在视锥体里面的面会被渲染,以外的则不会。这里要注意一个细节,当一个面(3个点)中,只要有一个点被包含在视锥体里面,整个面都会被渲染。

Clip Project Viewport Cull 1
空间剪裁
Clip Project Viewport Cull 2
空间剪裁 - Unity实例

 

 接下来到 Points Lines Polygons 图元(点线面)。经过上一步空间裁剪,淘汰并筛选出来的这些被包含在视锥体里面的图元(点线面)。

 这些被筛选的图元紧接着进入 Rasterizer 光栅化。敲黑板,这步非常重要。光栅化就是显卡上有一个叫光栅器的东西,它将上一步里图元中的顶点所包围起来的三角面的范围,这个范围映射覆盖了屏幕上的哪些像素,而在每一个三角面中所覆盖包含到的每一个像素,记录并生成一个“片元”。值得注意的是,这个片元并不是真正意义在屏幕上显示的像素,它储存了颜色、深度、坐标等信息。而真正变成屏幕上显示的像素,需要到后面的逐片元操作步骤。简单来说,光栅化就是将顶点转化为像素的过程。 

Rasterizer
光栅化

 

Fragment Processor 片元处理(着色器)。这个跟顶点处理很相似,它输入的是上一步传输过来的片元信息,在这一步进行处理加工(如何加工,我们后面再讲),之后也是输出一个个片元。

Per-Fragment Operations 逐片元操作 接收上一步传输过来的片元后,对这些片元进行进一步的处理,比如深度检测(前后的遮挡关系,哪些颜色不显示,哪些显示),半透明检测和混合(半透明的颜色和颜色的混合,这个混合就像Photoshop里面图层的混合模式)。最后输出在屏幕上面显示的每一个像素的最终颜色。

经过 Framebuffer Operations帧缓存操作,将每个片元的最终颜色放入 Framebuffer 帧缓存中。而在帧缓存中有着与屏幕上每一个像素对应的储存单元,你可以把帧缓存想象成一个收纳盒子,里面的每一个单元就是收纳盒里面的一个个小格子,而最终输出的片元将会对应的储存到每个小格子里面。当所有片元储存完成后,整个帧缓存(收纳盒子)就完成了一帧图像,于此同时,整个渲染管线算是结束。再到后面,显卡会根据帧缓存往显示屏上输送图像,最终呈现出我们在屏幕上看到的画面。

除了游戏开发,渲染管线可以说是跟大家息息相关的底层技术。大到电脑系统,小到某软件的界面,只要是你能在屏幕上看到的一切东西,都离不开渲染管线。所谓有渲染,你才能看到图像嘛。

 

A(顶点着色器)和B(片元着色器)就是我们可以用代码进行编辑和修改的地方,也就是我们通常说写Shader的地方。

首先我们看顶点着色器,你可以在这一步算阴影,改变顶点位置(比如做波浪效果)。但这步最重要要干的事情就是“空间转换”。举个例子方便理解,我们从Maya这类的建模软件中导出一个模型,导出模型的坐标其实还是Maya本地空间的坐标,而顶点坐标也是对应本地空间坐标的。但如果我们导入游戏引擎,比如Unity。你就必须把模型本地空间的坐标转换为Unity世界空间的坐标,否则顶点将依旧依照本地空间坐标来计算,当你在Unity里面移动模型时,坐标就会出错。就好像你家沙发在你家摆放的位置,依样照搬到别人家肯定不合适,铁定需要重新对位置进行调整。OK,把模型顶点的本地空间转化为Unity的世界空间后,我们还需要做进一步转化。因为我们是通过摄像机(Camera)去观察这个三维世界的,最终也是通过摄像机去呈现出画面,所以此时的顶点在Unity的世界坐标需要进一步转化为相对摄像机位置的“相机空间”里面的坐标,这个坐标转换就是把摄像机的位置作为(0,0,0),模型的顶点坐标相对这个(0,0,0)的位置进一步转换自己的坐标。OK,模型世界空间的坐标转换为相机空间的额坐标后我们还需要最后一步坐标转换。为什么还需要呢?回忆上面空间裁剪的步骤,我们是不是了解到一个叫“视锥体”的东西,在视锥体内的顶点的坐标(也就是相机空间坐标)还是一个三维的坐标,这个时候我们需要把它投影映射到二维屏幕上,就好像你拿着相机拍了一张照片。我们把这个三维空间中的坐标转换为二维屏幕的坐标,这里我们也把投影映射到二维屏幕上坐标叫做“投影空间”的坐标。这个时候的坐标就跟后面我们所讲在屏幕上像素的位置是一一对应的。

其次,我们看到片元着色器。这一步实际就是拿到光栅化后得到的片元,你可以写Shader对这些片元进行各种处理,最终得出一个个片元对应的颜色值(RGB),输出到下一步。

 

看了这么多理论,我们最后参照下图再重新整理一遍。

Render pipelines
渲染管线

这是一个简略的过程图,我们从最上边看起。首先我们导出的模型,其实就是一堆顶点数据和索引数据。这些数据导入到游戏引擎,就来到顶点着色器这一步,这步主要干的事情就是空间转换,把三维软件的本地空间转换游戏引擎的世界空间,最终转换为投影空间。紧接着顶点会按照索引里面的规则组装成一个个图元(包括三角面、线、点),这些图元会在相机空间中经历一次淘汰,也就是说在相机视野内的图元会被渲染,反之抛弃。紧接着筛选出来的这些图元会进入光栅化阶段,在这个阶段,图元中的三个顶点在投影空间中会组成一个个三角面,这些面中所包围的每一个屏幕像素会生成一个片元,生成的这些片元会在片元着色器中进行进一步加工处理,加工处理完后进入逐片元操作,这些片元将进行最后的检测,其中包括半透明检测,颜色混合。最终,每个片元将输出一个颜色值保存帧缓存中,显卡将会取出在帧缓存中对应屏幕像素位置的颜色值输送给屏幕,也就是最后呈现在我们眼前的画面了。

 

以上的知识会显枯燥,但如果你要学习Shader,理解渲染管线的原理是必不可少的。希望胡罗舶的同学们加把劲啃下来,期待下次分享!