【UnityShader从零开始】写在前面
发表于2015-08-06
很多人在学习Unity的时候对Shader都是一知半解,我在这方面也是个新人,接触Shader的时间也并不长,正因为是新人才能体会到学习Shader时候所遇到的困难和迷茫,无奈于资料不好找,网上难得的几篇教程讲的又不够完善或者太浅太短,所以我一直以来就想写一系列UnityShader的学习教程,在Unity这个圈子里我还是一只很菜很菜的菜鸟,感帮助过我的人,书,以及在网上无私奉献宝贵知识的前辈们,仅以此系列向他们致敬.
这个系列的教程会不定期的更新,由于笔者平时需要工作,有时可能会一段时间不更新,还望各位谅解,本人才疏学浅,可能在大神们面前班门弄斧了,写这系列教程一来是想帮助那些准备接触Shader却不知如何入门的同学们分享自己的经验,二来是借由自己写教程来锻炼自己写技术文章的文笔和巩固自己学习的知识,查缺补漏,只有你能把你学到的知识透彻的讲给别人听的时候,你才完全掌握了它.由于接触Shader时间并不长,难免会有所纰漏和错误,还望各位看官不吝赐教,批评指正.
前言
前言部分主要是讲述一下个人对学习UnityShader的一些建议,以及描述一下图形渲染的大概模型和UnityShader的工作方式.
首先要说明一点,要想学好Shader而不只是停留在会用几个简单语法的层面,那你就一定不可避免的需要接触数学,主要是线性代数,深入研究可能会用到微积分和空间几何,要做好心理准备,总之要学习Shader不难,但要想学好,你真的要花一番功夫.再有一点就是编写Shader来说目前只有一些有限功能的带代码提示的编辑器,还没有可以调试的工具,一旦Shader出错Unity给出的提示经常是非常不明确的,这时候你只能耐心的一行行分析了.
先说明的一下什么是Shader,Shader一般被翻译为"着色器",他并不是指某一种具体语言,他是一种技术,可以让程序员去通过编写代码去参与到GPU渲染图形的具体流程中去,去制作一些精美的特效和动画甚至是让模型发生奇怪的形变,简单点就是可以把它理解成一种对呈现到屏幕上画面的一种美化工具,想一想是不是很酷,你竟然可以去参与显卡的工作.以前写一辈子Hello world一直和CPU打交道.
而编写Shader目前比较知名的是基于DirectX的HLSL和基于OpenGL的GLSL,而我们Unity用的ShaderLab是基于Nvida和Microsoft一同开发的CG(C for Graphic)语言,从名字上我们就能知道,只是一门类C的语言,如果你接触过C语言或者C++,甚至是JAVA,C#那对他的基本语法一定不会陌生,CG语言他是跨平台的,他可以基于OpenGL也可以DirectX来运行,也就是说他是工作在DX和OpenGL上层的抽象语言,这三种语言在某些具体方面可能有性能和扩展性上的区别,但是对于背后的实质技术并无过大区别,我对GLSL和HLSL接触的不多,HLSL由于是基于DX的可能现在很多高端引擎都是用HLSL,GLSL的话优势可能在于跨平台吧.
好吧我们只要关注我们的CG就可以了,有兴趣的同学可以自己去了解一下另外两种语言,说明一下由于CG是Nvida和微软共同开发的,所以CG上的很多标准和HLSL是兼容的,学起来有很多互通的地方.Unity 使用的并不是完完全全的CG语言,如果你看官方的API你会发现他的Shader分为三大类:
(1)Fixed function shader 固定渲染管线 Shader, 现在的使用已经很少了,一般用来做默认处理,以一种固定的模式去处理渲染流程,但是他完全使用ShaderLab语法让你很简单的去修改一些参数,功能有限,很多Unity内置的默认效果用的是这个Shader.(2)Surface Shader 不知道该怎么翻译,姑且叫表面Shader,这个Shader是Unity官方文档里用的最多的了,而且在国外很多的UnityShader学习教材里也很多用它来举例子,怎么说呢,这个Shader其实和第三种已经很像了,只不过你并不用全部把片段着色器代码自己来写,unity给你自带了很多封装好的,如果你想要自己写也可以,我个人感觉他和第三种最大的区别在于它的光照模型我们不能确定到底是外面哪个光源,我没试验过是否可以通过传外部参数来让解决.另外在Surface Shader中的同一个SubShader中不可以使用多个Pass.
(3)Vertex and Fragment Shader 这类是我主要要讲的Shader,很好很强大,定点着色器和片段着色器完全由我们自己来控制,这样子我们就能最大化使用我们掌握的东西,当然这类Shader也是最需要时间学习的.
我们来看一下GPU把3D物体渲染到2D屏幕上的一个简略的步骤(只是简单地有个印象),以后具体的地方会具体分析:
1,由于我们导入Unity的模型或者我们再Unity创建的模型,它们自身都有一个属于自己的坐标系,就像人一样,无论你站在什么方位,你总是知道自己的前后左右是什么方向,而这个前后左右就建立在你自身的坐标系,所以Unity里每一个模型并不知道其他模型的坐标系,那么为了方便计算和操作我们就要把他们转换到一个统一的空间坐标系里,前者是物体的模型空间,后者是世界空间,这是第一个空间变换:"模型空间"->"世界空间",然后我们要确定我们在摄像机里具体能看到哪些东西,同样为了方便计算和处理,我们要再一次把物体从世界空间转换到摄像机空间,其实就是以摄像机为坐标系原点建立一个三维空间,经过这一步操作就能知道每一个物体从摄像机的位置来看他在什么方位,这是第二个空间变换:"世界空间"->"摄像机空间".现在虽然知道了物体在相机空间的位置,但是还不能立刻进行渲染,为了方便后面要进行的空间裁剪和,屏幕坐标映射当然也是为了方便计算,我们要把摄像机空间内所有物体的坐标再一次转换到一个坐标范围是(-1,-1,-1)到(1,1,1)的正方体空间区域中,也就是整个空间变换中最难理解的"投影变换",这是第三个空间变换:"摄像机空间"->"投影空间".接下来就是把投影空间里的物体映射到屏幕坐标上去.第四个空间变换(严格来说这不应该叫空间变换):"投影空间"->"窗口空间".以上大概属于空间变换部分的内容,很多书本上的地方叫法和说法都不尽相同,理解大概意思即可.
2,显卡来处理图形的过程中一般有三个最基本的要素:点,线,面(一般指三角面),而在上一步中我们只是针对模型中单个顶点来一一处理,接下来我们就把经过空间变换剩下来的顶点(为什么说剩下来呢,因为上一步有一点没有提,就是在投影变换的过程中, 会进行一步剔除处理,把一些不在显示区域内的顶点根据一定规则过滤点,这样可以减少接下来的运算量)进行组装,组装成点,线,面.也就是所谓的图元装配,图元装配进行完之后,我们需要进行栅格化:大家都知道我们面前的屏幕是由像素矩阵构成的,而我们处理的模型只是由有限的顶点构成的,经过图元装配后形成的基本图元(点线面),我们要把它和屏幕上的像素区域对应起来,也就是进行像素填充,一般模型的每个顶点都会带有一些基本信息(例如,法线,位置,纹理坐标,颜色等),即一个三角面其实只有三个顶点是有基本属性的,而其他填充进来的像素区域是并不直接拥有这些基本属性的,他们的属性都是通过在顶点间差值计算得来的(其实上面提到的像素应该称为片元fragment).有些书籍中把图元装配也归到栅格化中,请注意.经过这些处理模型基本在GPU中已经形成了他所要绘制到屏幕上的样子了.不过这并没有结束.
3,经过上面的处理我们就仿佛得到了一个预备要绘制到屏幕上的临时数据区域,不过到底是将这些像素是否最终绘制到屏幕上,怎么绘制到屏幕上,还需要经过一些判断.制作游戏中最常见的现象,离摄像机近的物体要绘制在前面,会把离摄像机较远的物体挡住(不考虑半透明物体),如果没有某些判断的话,那如果GPU先渲染了离屏幕较近的物体,而后处理了离屏幕较远的物体,那么近的先画到屏幕上,远的后画就会把之前的覆盖掉了,这显然不是我们需要的效果(并不是所有引擎都可以自由的去控制物体的渲染顺序,即使可以控制,很多时候也十分麻烦,并不十分常用).所以说我们就需要在绘制的时候进行检测,上面这个问题使用的是ZTest(还有很多检测这里只提这一个,其他的以后遇到了再具体说),对于GPU来说拥有两个最基本的缓冲区:帧缓冲区和Z缓冲区(也叫深度缓冲区),这两个缓冲区都与屏幕上的每个像素一一对应,其中帧缓冲区对应着屏幕上的像素点的最终颜色,GPU最终都是通过把颜色写入这个区域来呈现在屏幕上的,而另一个深度缓冲区则存储着屏幕上每个像素的深度值,所谓的深度值就是离摄像机的远近(一般都被规范化为一个0~1的数值),数值越大说明离摄像机越远,刚才我们提到的ZTest就是通过将光栅化后的像素的深度值和当前屏幕上像素对应的深度值进行相应的比较,来绝定是不是把新的像素点覆盖掉原来的像素点(当然这并不是最终是否写入帧缓冲区的判断条件,还有一些其他判断),至于以什么模式来判断并无硬性要求,根据你具体的要求来选择判断方式.经过这一系列的判断和处理之后GPU会把最终结果写入帧缓冲区,我们会在下一帧看到刚刚处理过的最新画面了
在上面我简略的说了一下GPU渲染3D物体的大概流程,那我们的Shader是怎么参与进来的呢,开篇已经说过了,这个系列的文章只以Vertex and Fragment Shader来进行距离,这是Unity中最复杂和最强大的Shader,另外两种大家自己跟着官方文档了解一下吧.
在一个shader中我们主要通过两个部分来参与渲染,顶点着色器(vertex)和片段着色器(fragment),他们两个参与的时机不同,VertexShader是在顶点变换的时候,而FragmentShader是在光栅化到将最终计算完成的像素值写入帧缓冲区。在VertexShader中我们可以对模型顶点实施一些坐标转换甚至形变,也可以通过计算和赋值把一些片元默认并不含有的属性带入FragmentShader来进行一些需要的操作.而在FragmentShader中我们可以做的就很多了,很多光照处理都是在这里进行的(当然在VertexShader中也可以进行光照计算,不过效果不是十分好),而且也可以做一些比较复杂的色彩变换和处理.好了前言就说到这里,我第一次写关于Shader的东西,语言组织的不是很好,大家即使理解的不太清楚也没有关系,有个大概的了解
本篇只是接下来系列教程的一个前言,做一些基本交代,接下来为了大家更好的理解接下来的教程,也为了减少浪费一些不必要的时间在一些过于基础的语法上,提议大家自己先去网上找一些资料或者跟着官方的文档写一写简单的Shader,熟悉一下基本语法,记得要写Vertex and Fragment Shader,另外两种也可以看看.相信我提前预习才不会看的一头雾水,本人会不定期更新,希望大家能有所收获.
再给大家推荐一些参考资料,我学习的很多知识也来自与它们(去网上搜搜吧):
1.《gpu编程与cg语言之阳春白雪下里巴人》,挺通俗易懂的,是一位前辈个人总结的,不妨一看
2.《Cg Programming in Unity》,维基教科书的整理的一些关于Unity图形渲染的知识,我还没怎么看,同事介绍的
3.《Cg教程_可编程实时图形权威指南》,一本比较早的书,书上的编排有些晦涩,并不十分适合UnityShader来入门,语法上是纯CG的与Unity略有不同.
好了,在下才疏学浅难免有所纰漏,希望各位能及时指正,大家一起进步,若鄙人这些粗浅的见识能够帮助到大家,我将深感欣慰.也欢迎大家继续关注《UnityShader从零开始》系列文章.