版本:unity 5.4.1 语言:Unity Shader
总起:
最近花了一个月的时间把《Unity Shader 入门精要》看完了,没怎么写博文,因为写得太好了,看得有点废寝忘食了,再次强烈推荐。
今天把写Shader前必须要知道的渲染流水线给概括一下,然后简单结合顶点\片元着色器Shader,说说各个代码在流水线的位置,以及职责功能。
渲染流水线:
在写Unity脚本的时候,不管是C#也好还是js也好,都是在跟CPU打交道,做算术运算、调用类成员、控制程序流程。而写Shader不同,他是跟GPU打交道。
我们首先来看看脚本的一个调用流程:(百度上有中文翻译,我这边直接上官方网站了)https://docs.unity3d.com/Manual/ExecutionOrder.html
从这里看出游戏中的一帧首先是初始化,也就是Awake、OnEnable、Start这些函数的执行,接着物理、输入、游戏逻辑的执行,在Unity中写代码的一天到晚都是跟这些函数打交道吧?接下来我们看到了Scene Rendering,这里就是一帧最重要的渲染部分了:由CPU发出指令(也就是平常所说的drawcall),GPU响应指令进行渲染,执行的就是一个渲染流水线。
而Shader在其中对一个渲染流水线进行控制,告诉GPU,CPU传过来的这个物体该如何渲染。粗浅的来说Shader之于GPU,就如同C#之于CPU。
那么渲染流水线是怎样的一个流程呢?简单的来说就是:“顶曲几裁屏,三三片逐屏”。
这是一种简单的记忆方法,感觉要还蛮好用的。
第一行所说的是几何阶段:顶点着色器 -> 曲面细分着色器 -> 几何着色器 -> 裁剪 -> 屏幕映射。主要的工作就是将模型的各个顶点变换到屏幕坐标的一个空间中,为真正的渲染做准备。
第二行说的是光栅化阶段:三角形设置 -> 三角形遍历 -> 片元着色器 -> 逐片操作 -> 屏幕图像。这一个阶段就是真正的在屏幕显示像素。
一个简单的Shader:
这边就通过书上的例子简单的说说流水线各个部分的作用。
Shader "Unity Shaders Book/Chapter 5/Simple Shader" { // 显示在Unity Inspector上的属性,给用户提供控制 // 形式:属性名("Inspector显示的标签", 属性类型) = 初始化数据 Properties { // 这是一个Color,初始化为(1,1,1,1),在Inspector上显示Color Tint,而在Shader中使用_Color进行调用 _Color("Color Tint", Color) = (1, 1, 1, 1) } // Shader的控制代码都在Pass中,一个SubShader可能包含多个Pass // 根据控制代码的不同可能会调用所有的Pass,也可能只调用一个 // 现在我们只有一个,进行渲染就会调用这个Pass,暂时不用多管其他的情况 SubShader { // 标签,SubShader下的标签对所有Pass都有效,而Pass中的只对本身有效 // Queue:说明渲染所在的队列,Geometry表明大多数不透明物体是在该队列中渲染的 // RenderType Opaque:表明这个物体是不透明的,与队列的区别是,Queue只是渲染顺序,即使设置成其他的也没问题, // 但你需要这个物体是不透明的RenderType就必须设置为Opaque Tags { "Queue" = "Geometry" "RenderType" = "Opaque" } Pass { // LightMode:光照模式 Tags { "LightMode" = "ForwardBase" } // 设置,控制Shader的一些默认处理,深度测试和深度写入总是开启 ZTest On ZWrite On // CGPROGRAM和ENDCG成对出现,两行内部的代码就是用cg语言(c语言的变种)写的控制代码 CGPROGRAM // vertex:表明控制顶点着色器的函数是vert // fragment:表明控制片元着色器的函数是frag #pragma vertex vert #pragma fragment frag // 申明Properties中的属性,以便在vert和frag函数中调用 uniform fixed4 _Color; // Unity给vert函数提供的输入 struct a2v { // vertex必须,POSITION表明他是一个模型的顶点坐标 float4 vertex : POSITION; // normal,NORMAL表明需要的是法线 float3 normal : NORMAL; // texcoord,TEXCOORD0表明需要的是当前的模型的uv坐标 float4 texcoord : TEXCOORD0; }; // vert函数的输出,frag函数的输入 // SV_开头的两个输出是必须的,而且必须这么写,这是流水线过程中必须获取的数据 struct v2f { // pos必须,SV_POSITION表明需要输出一个屏幕坐标 float4 pos : SV_POSITION; // color,颜色,其他属性,在frag函数中使用,不过一般冒号后面的标识为TEXCOORD0、TEXCOORD1、TEXCOORD2... // 只要不重复就行了,其他属性标明什么无所谓 fixed3 color : COLOR0; }; // 顶点着色器控制的函数 v2f vert(a2v v) { // 先初始化输出结构 v2f o; // UNITY_MATRIX_M:从模型空间到世界空间 // UNITY_MATRIX_V:从世界空间到相机的观察空间 // UNITY_MATRIX_P:从观察空间到屏幕空间 // o.pos:需要输出一个屏幕空间的坐标,所以很简单,UNITY_MATRIX_MVP是模型空间到屏幕空间的矩阵,然后乘以v.vertex就能得到 o.pos = mul(UNITY_MATRIX_MVP, v.vertex); // 根据法线计算颜色 o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5); // 输出 return o; } // 片元着色器使用的控制函数,必须写上SV_Target,表明该输出是一个屏幕上的颜色 fixed4 frag(v2f i) : SV_Target { // 获取vert处理后的颜色,再与_Color.rgb相乘 fixed3 c = i.color; c *= _Color.rgb; // 最后输出 return fixed4(c, 1.0); } ENDCG } } // 申明备用的Shader,如果以上Pass无法运行的话 Fallback "Diffuse" }
最重要的是vertex和fragment所指定的两个函数vert和frag。它们所在的流水线位置分别是顶点着色器和片元着色器。
顶点着色器,对应vert函数,正如上文所说SV_POSITION变量是必须的,顶点着色器最重要的职责就是根据一个模型坐标输出屏幕坐标。
曲面细分着色器和几何着色器应该也是可以编写的,不过学这本书的时候没有用到,所以暂时忽略。
裁剪,vert把模型坐标变换到了屏幕坐标,这样就可以确定每个顶点是否在屏幕中,裁剪就是将不在屏幕中的顶点给裁剪掉。
屏幕映射,把每个图元的坐标转换到屏幕坐标中,经过MVP矩阵后,坐标是在(-1,-1)到(1,1)中的,而屏幕映射就是把它映射到屏幕分辨率中。比如分辨率为1920*1080,则需要把(-1,-1)至(1,1)映射到(0,0)至(1920,1080)。
三角形设置、三角形遍历。顶点组成三角形片,三角形片才组成网格,组成整个模型。这个两个阶段就是将顶点所组成三角形所覆盖的像素计算出来。
片元着色器,对应frag函数,对上两个阶段中传来像素进行处理,最终需要输出一个SV_Target所代表的最终颜色。
逐片操作,做深度测试、模板测试等,通过的像素才能显示在屏幕上,可以配置。对应上面的代码ZTest On开启深度测试。
屏幕图像,最终显示的结果。
总结:
顶曲几裁屏,三三片逐屏。记住这个的同时,需要知道vert函数做的是顶点变换对应顶点着色器这个阶段,而frag函数是对每个像素颜色进行处理,对应的是片元着色器这个阶段。
而裁剪和逐片操作这两个阶段虽然无法编写代码,但可以进行控制。
今天用自己的语言简单概括了一下渲染流水线,不知道大家能否通过这篇文章对渲染流水线有个简单的认识。写Shader,不知道流水线写的时候就根本不知道为什么要这样去写,所以掌握这个流水线是非常必要的。
以后会给大家带来更多的Shader理解和一些有趣的Shader使用。