Unity Shader读书笔记(1)

前言

        今天我本来想用Unity自己做出点东西。我本来设想渲染的Shader也要我自己去写,做一个简单的phong模型就好了。然后我发现半年没写Shader已经全部忘记,本来我想找一下自己当初写的读书笔记。而之前的读书笔记我觉得当时写得不好所以没有上传,又加上我换了电脑,所以我根本找不到之前写的读书笔记。这次我重新再学一遍,再做一次读书笔记。(这次无论写的好不好,我都上传一次。不然又要看那三百多页的书太难了。)

渲染的流程

        渲染的流程大致可以分成三个阶段:应用阶段(Application Stage)、几何阶段(Geometry Stage)、光栅化阶段(Rasterizer Stage)。

应用阶段

        这个阶段的主导是应用程序,所以这通常是由CPU来负责的。在这个阶段我们对其有绝对的控制权。(这里的控制权是表示对数据的控制操作,在GPU中的流程部分数据的处理是由生产此GPU的厂商进行封装,换句话说我们对于这部分的数据处理是没有控制权的。)

        在这个阶段我们有三个主要的任务:首先我们要准备好场景数据,即我们要摆放好相机的位置、视锥体、场景中的模型和光源等。然后我们要将相机中看不到的物体剔除出去,这一步的目的粗粒度剔除物体以提高渲染性能。最后我们要设置好每一个模型的渲染状态,这些渲染状态包括但不限于它使用的材质(漫反射颜色、高光反射颜色)、使用纹理、使用的Shader等。这个阶段的最后是输出渲染所需要的几何信息,即渲染图元(rendering primitives)。通俗来说,渲染图元可以是点、线、三角面等。而这些信息会被传输到下一个阶段——几何阶段中进行处理。

       应用阶段中的三个阶段:

(1)把数据加载到显存中

        CPU要将渲染所需的数据从硬盘加载到内存,然后再将内存的数据装载到显存。GPU对显存的访问速率大于内存而且大多数显卡是没有内存的访问权限。

(2)设置渲染状态

        所谓渲染状态通俗来说就是定义了场景中的网络是如何被渲染的信息。比如如果我们没有对Unity的物体更换Shader,那么Unity场景中的物体都是走Unity默认的Shader。如果我们改变其中的一个物体的Shader,那么这个物体就会按照我们给定的Shader进行渲染。

(3)调用DrawCall

        DrawCall是CPU调用GPU的命令。当CPU使用一个DrawCall时,GPU会被调用。GPU根据渲染状态(例如材质、纹理、着色器等)和顶点数据进行计算最终显示在屏幕上。

几何阶段

        这个阶段是在GPU上执行,在这个阶段GPU会接收应用阶段CPU传递过来的信息然后决定需要绘制的图元是什么和怎么样去绘制它们。这个阶段最主要的任务就是将需要渲染的顶点坐标变换为屏幕坐标,变换后我们会得到转换后屏幕空间的二维顶点坐标、其对应的深度值和着色等相关信息。这些信息会传递给光栅化阶段做为其要处理的数据。

        几何阶段可以细分为以下的几个阶段:

(1)顶点着色器(Vertex Shader)

        它通常用于实现顶点的空间变换、顶点着色等功能。它是完全可编程。

        它的输入来着CPU,其处理的单位是顶点。每一个CPU传入的顶点信息都会调用一次顶点着色器。其本身不能创建或是销毁顶点,而且它也得不到顶点与顶点间的关系。

        其主要的工作就是坐标变换、逐顶点光照和给下一个阶段传入数据。

        坐标变换:顶点着色器最基本的工作就是将游戏空间中的顶点坐标转变齐次裁剪空间下的坐标。除此以外,我们也可以将坐标先进行某种变换后在转变到齐次裁剪空间下。(当然我们也可以反着来,但是很少这么做。)Unity Shader中你可以看到这样的代码:

1
ver.pos = mul(UNITY_MVP,v.position);

这段代码就是将顶点坐标转变为齐次裁剪空间下的坐标。完成坐标转换后,硬件还会做透视除法,最终得到归一化的设备坐标(Normalized Device Coordinates, NDC)。

PS:OpenGL中NDC,z分量是在[-1,1],而DirectX是[0,1]。Unity在这里和OpenGl一致。

(2)曲面细分着色器(Tessellation Shader)

        它通常用于细分图元,但是他是一个可选的着色器。它是完全可编程。

(3)几何着色器(Geometry Shader)

        它用于执行逐图元(Per-Primitive)的着色操作,或者被用于产生更多的图元。他是一个可选的着色器且完全可编程。

(4)裁剪(Clipping)

        这个阶段的目的是将那些不在摄像机视野范围内的顶点裁剪掉,并剔除一些三角图元的面片。在这个阶段我们不能编程但是可以配置。

(5)屏幕映射(Screen Mapping)

        这个阶段负责将每一图元坐标转换到屏幕坐标上。这个阶段是不可配置和编程的。

        这个阶段所接收到的数据仍然是三维坐标体系下的数据(在NDC范围内的三维坐标)。而这个阶段就是将这些坐标的x和y坐标变成屏幕坐标系下的坐标。屏幕坐标和我们用于显示画面的分辨率有很大的关系。z分量作为深度,这个阶段不会做任何的处理。但是实际上屏幕坐标系和z轴一起构成了一个坐标系名为窗口坐标系,这些数据会被一起传递到光栅化阶段。(OpenGL把屏幕左下角当做窗口坐标,DirectX则是左上角。Unity这里与DirectX一致。)

PS:虽然GPU已经对我们开放了很多控制权,但是我们还是不能对其进行完全的控制。

光栅化阶段

        这个阶段也是在GPU上执行,它会接收几何阶段处理的信息(屏幕坐标系下的顶点位置以及和题目相关的额外信息,如深度值(z坐标)、法线方向、视角方向等)。然后它根据这些信息去做决定每一个渲染图元中的哪些像素可以被渲染。接下来它再对这些处理后的数据进行插值做逐像素的处理,最终渲染到屏幕上。光栅化的两个最重要目标:计算每一个图元覆盖了哪些像素,以及计算这些像素的颜色。

        光栅化阶段可以细分为以下的几个阶段:

(1)三角形设置(Triangle Setup)

        上一个阶段(这里指的是几何阶段)输出的都是三角网格的顶点。如果要得到整个三角形对像素的覆盖情况,我们必须计算每条边上的像素坐标。为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。这样一个计算三角形网格的过程就称为三角形设置。

(2)三角形遍历(Triangle Traversal)

        这个阶段我们会检查每一个像素是否被一个三角网络所覆盖,如果被覆盖就会生成一个片元(fragment)。而找到哪些像素被三角网格覆盖的过程就是三角形遍历。这个阶段也被称为扫描变换(Scan Conversion)。

        上一个阶段,我们其实只得到了一个三角形中的三个顶点的信息,但是它们有可能覆盖了很多的像素点。这些不是顶点而被覆盖的像素信息一般对三角形三个顶点进行插值运算获得其中的数据。

        这一步的数据就是一个片元序列,一个片元并不是一个像素而是包含很多状态的集合,这些状态用于计算每一个像素的最终颜色。这些状态包括但不限于屏幕坐标、深度值、法线等信息。

(3)片元着色器(Fixed-Function)

        这阶段用于实现逐片元(Per-Fragment)的着色操作。它可编程。(在DirectX中它又被称为像素着色器——Pixel Shader)。它会接收上一个阶段(三角形遍历)的输出然后得出一个或是多个的颜色值。

(4)逐片元操作(Fragment Shader)

        这个阶段负责执行很多重要的操作:修改颜色、深度缓冲、进行混合等。它不可编程但是有很高的可配置性。(在Direct中这一步被称为输出合并阶段)

        这一阶段有几个主要的任务:

        (1)决定每一个片元的可见性。

        这里可能有点疑惑,明明我们在几何阶段中已经进行了一次裁剪为什么这里还要再判断一次呢?其实几何阶段中的裁剪是将哪些不在NDC中的图元排除掉,但是不一定所有在NDC的中图元都是可以被看见的。这里就涉及到物体间的遮盖问题,当一个物体片元被另外一个物体片元遮盖住时,我们完全是不用进行渲染的。为了排除这样的片元,我们要进行很多的测试:例如模板测试(Stencil Test)和深度测试(Depth Test)。

  • 模板测试(Stencil Test)

            模板测试与之相关的就是模板缓冲(Stencil Buffer)。GPU会读取改片元位置的模板值,然后将该值和读取到的参考值进行比较(这个比较函数可以由我们开发者决定)。如果没有通过这个测试,那么这个片元就会被舍弃。而模板缓冲的值我们是可以随意修改的,我们不需要考虑这个片元是否通过测试。

  • 深度测试(Depth Test)

           深度测试开启后GPU会将该片元的深度值(也就是之前没有用到的z值)和深度缓冲区中的深度值进行比较(这个比较函数可以由我们开发者决定)。如果没有通过这个测试,那么这个片元就会被舍弃。与模板测试不同的是,深度缓冲区的值是只有这个片元通过了深度测试,它才可以修改深度缓冲区的值。

PS:这些测试逻辑上是在片元着色器上进行,但是有时候为了性能优化有些测试会提前进行。毕竟片元着色器要计算出片元的颜色也是要花费一定的性能,而这些经过处理的片元最后是不需要显示的,那么这些性能就被浪费了。所以有时候GPU会提前处理这些测试。但是这样做也是存在问题,比如针对透明物体的时候。

        (2)存储通过所有测试的片元颜色值到颜色缓冲区中进行混合。

(5)屏幕图像(Per-Fragment Operations)