作者:esfog

原文:http://www.taidous.com/thread-25286-1-1.html

 

距离上次首篇前言已经有一段时间了,一直比较忙,今天是周末不可以再拖了,经过我一段时间的考虑,我决定这一系列的教程会避免过于深入细节,一来可以避免一些同学被误导,二来会避免文章过于冗长难读, 三来可以让大家有更多自己思考的时间.如果我想讲述一些细节问题会另开一个系列.
UnityShader语法实例浅析
  上一次我在前言里大体上讲述了一下图形渲染的流程以及Shader是如何参与的,我们这系列教程还是更注重实际应用多一些,所以这一节为了以后打基础,我们来分析一下UnityShader的语法结构.如果没有看过前言的同学,我建议你还是先去看看,一遍看的效率更高,链接是http://www.cnblogs.com/Esfog/p/3534435.html.

  我们先来看一段十分简单但是完整的Shader代码.本系列教程如无特殊声明,所有UnityShader都是Vertex&Fragment Shader.

[AppleScript] 纯文本查看 复制代码

Shader "Esfog/SimpleShader"
 {
    Properties 
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }

    SubShader 
    {
        Pass
        {
            Tags { "RenderType"="Opaque" }
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            uniform sampler2D _MainTex;

            struct VertexOutput
            {
                float4 pos:SV_POSITION;
                float2 uv_MainTex:TEXCOORD0;
            };

            VertexOutput vert(appdata_base input)
            {
                VertexOutput o;
                o.pos = mul(UNITY_MATRIX_MVP,input.vertex);
                o.uv_MainTex = input.texcoord.xy;
                return o;
            }

            float4 frag(VertexOutput i):COLOR
            {
                float4 col =  tex2D(_MainTex,i.uv_MainTex);
                return col;
            }

            ENDCG
        }
    } 
    FallBack "Diffuse"
}


好下面我们来一点点分析这段代码,先给大家一些建议,如果遇到暂时不理解的知识可以合理性的选择暂时跳过,本人也不会面面俱到,很多知识点要在如后具体结合实例才能讲清楚,希望大家理解,知识的学习不是一蹴而就的,随着不断的积累,很多以前不理解的地方,慢慢都会理解的.不过首先你要能坚持和有一颗探索的心.
   第1行代码的 Shader "Esfog/SimpleShader" 是类似于HTML中的<html></html>一样,是整个Shader最外层的结构 ,在后面的引号中你可以给你的Shader起个名字,而且不必和你的Shader文件名字一致,其中的左斜线是目录的分隔符,起了名字之后你就可以在材质上选择Shader的时候通过这个名字找到这个Shader了.
   第3~6行代码是定义Shader一些可调节参数的地方,定义之后,你在Unity中选择这个材质,然后在Inspector面板中通过调节这个变量来影响Shader的展示效果。很多时候我们不能完全预估好所有的参数值,所以我们需要把这些变量暴露出来以便美术同学或者是程序自己来通过调节来达到自己想要的效果,这里面目前我们只定义了一个2D贴图变量.我们来解释一下这行代码 _MainTex ("Base (RGB)", 2D) = "white" {},其中_MainTex(这个名字也是随意的,但建议使用下划线开头)是我们在Shader内部来识别这个变量的。后面的Base(RGB)是展示在Inspector中的名字,这个名字随便起。跟在后面的2D是设置了我们这个变量的类型,在Unity的帮助文档里你可以去看一下有很多种,例如float,color,range等等.我就不过多描述了,而等号后面的是为这个变量设置一个默认值,每种变量类型默认值设置方式不一样,2D贴图就记住这么设置吧.
  第8行的SubShader要着重解释一下,在一个Shader中可以包含任意多个SubShader,但是只有一个SubShader被显卡选中并最终执行,之所以这么做,是因为我们的Shader代码可能运行在各种不同的机型上,而各个显示渲染设备都有一定的不同,所以我们经常在不同的SubShader中写针对不同渲染能力设备的Shader代码,到时候显卡会自动选择一个最适合它自身性能的SubShader去执行,如果我们写的SubShader最后都没有被选中的话,那么我们看43行代码, 这时候就会默认去执行FallBack后面跟着那个Shader了.当然一般情况下自己在自己电脑上学习写一个SubShader就够了.
  第10行的Pass也很重要,在一个SubShader中我们可以定义多个Pass,而Pass与SubShader不同,你写的每一个Pass都会按照顺序全部执行一遍.我们要在Pass中写具体的着色器代码.为什么会有多个Pass呢,可能是为了达到一些特殊效果.你在看一些网上大神的Shader经常会有多个Pass的.还有一点要提一下,在Unity主推的Surface Shader中是不能写Pass的,因为在Unity的光照模型中他会自动定义一些Pass,所以也就不再允许你额外写了.
  第12行的 Tags { "RenderType"="Opaque" } 他是一种ShaderLab(UnityShader中除了CG以外的语法)给我们提供好的可配置的一些选项,这些选项你可以每个Pass都写一个,也可以直接写在SubShader下面让所有Pass公共用一个配置。这样的配置除了Tags还有Cull,Blend,ZTest等等.他们都很重要,我们会在以后的章节中具体问题具体讲解,有兴趣的同学可以自己去看一下文档,里面都有.我们先简单说一下这个Tags,Tags里面的语法很类似于HTML中定义标签属性的方式,它是用来配置一些渲染参数的,这里面的RenderType = "Opaque"是告诉渲染设备,这个使用这个Shader的材质是一个不透明的物体,这有什么用呢?是这样的,在场景中有存在着大量的半透明和不透明的物体,对于不同类型的材质在渲染上会有一些差异并最终导致我们展示在游戏场景中的效果是否正确.
  第14~40行被CGPROGRAM 和 ENDCG包含的部分才是真正的CG代码,我们的顶点着色器和片段着色器也就要写到这里面.我们来好好继续一点点分析
  第15~16行的两句话是告诉渲染设备顶点着色器(vertex)和片段着色器(fragment)的名字是什么,这是为了让显卡在渲染的时候能够准确找到他们而必须声明的.而我们后面具体些着色器的时候也要用这个名字(不一定是vert和 frag,随便起,只要保证你后面的和你生命的名字一致就可以).
  第17行#include "UnityCG.cginc"是因为Unity为我们提供大量的可用变量和常量,比如说一些空间变换矩阵,这样子可以提高我们的开发效率,为了使用他们你需要把这句话加上。在Unity4.0之后你可以省略这句话了,他会被默认包含进来,不过为了向下兼容还是建议加上。
  第18行的uniform sampler2D _MainTex;我们声明了一个叫_MainTex变量,他的类型是sampler2D(也就是指可采样的2D贴图),而前面的uniform是可省略的,意思是说这个变量是由外部赋值进来的,为了严谨性,建议加上,你会发现这里的_MainTex和我们的Properties中的_MainTex名字一致,前面说过了,通过这种方式我们就可以在着色器中使用在Properties中声明的变量了。当然外部提供变量的方式不止Properties一种,还可以通过脚本来直接对Shader的变量赋值,以后具体应用具体说.
  第20行~24行是声明一个顶点着色器的输出结构,它同时也是片段着色器的输入结构.当然你也可以不利用结构体,而通过out来返回结果,为了代码的可读性还是用结构体为好.就跟C语言里面结构体差不多,类型上CG和C语言有一些差别,多了一些类似于float4,float2,float4x4这样的类型,更多的请参考Unity文档,不过要记住这是CG的基本类型,并不是符合类型,因为显卡的工作模式和CPU并不一样,这样类型的变量渲染设备处理起来非常方便。与C语言的最大差别在于每个变量声明最后都跟了一个像:SV_POSITION这样的语法,这在CG中称为语义(semantic),它们的作用是为了让Shader中的变量和显卡中的相应寄存器关联起来,这是跟渲染管线息息相关的.简单的理解就是显卡设定了一些特定的寄存器,用来存放一些指定的特殊变量,在渲染时候如果需要用到的话就直接来取,这样子可能处理起来更快也让像素着色器和顶点着色器之间交流起来更方面,这只是我的个人理解,更专业的描述请参考康玉之的《GPU 编程与CG 语言之阳春白雪下里巴人》。其中SV_POSITION是专门用来存放模型顶点在投影空间的坐标的,关于变换不是十分了解的可以参考我上一篇写的前言。TEXCOORD0可以用来存放任何你想存放的变量,在光栅化的时候这个变量会被插值 。类似于TEXCOORD0还有TEXCOORD1,TEXCOORD2等,具体个数看显卡的性能.我们在这个结构里添加了两个变量一个是顶点投影坐标,这个是必须有的,另外一个是顶点的纹理坐标,这个不是必须的,我是为了使用那张2D贴图来为模型上色而加上的,稍微解释一下什么是纹理坐标:纹理坐标也就是uv坐标,通常美术在制作模型的时候,为了把用2D贴图贴到一个3D模型上,就会为模型的每一个顶点指定一个纹理坐标,而这个纹理坐标是一个二维坐标,根据这个坐标去贴图上查找相应的颜色就可以了,在光栅化的时候纹理坐标又会被差值,最终我们只通过对几个顶点指定纹理坐标却让整个模型都添加了纹理颜色.对这里如果不太理解,请自行百度,或者选择跳过.
  第26~32行终于到了激动人心的时刻了,这是顶点着色器的代码部分,着色器(理解成函数就行了,不必纠结名字)的返回类型为我们刚才声明的返回结构,着色器名字就用我们在CG代码一开始通过#pragma 关联的名字,而参数这里要注意一下了,顶点着色器接受的参数都是模型最原始的参数,也就是美术同学指定的模型本身的一些参数,包括顶点的位置,法线,纹理坐标,颜色等等.而这些东西Shader是怎么知道的呢?可以在场景中新建一个Cube,可以看到要想被渲染到屏幕上的物体,都会包含一个MeshRenderer组件,通过MeshRenderer我们把顶点信息传递给了Shader,但是在Shader中并不是通过生命外部变量来接收的而是通过我们刚刚提到过的语义(semantic)来关联这些参数.而这个appdata_base是一个Unity为我们定义好的结构,就在我们上面写过的#include"UnityCG.cginc"包含进来.你可以在Unity安装目录的/Editor/Data/CGIncludes文件夹里面找打他看一看源代码.

[AppleScript] 纯文本查看 复制代码

 struct appdata_base {
     float4 vertex : POSITION;
     float3 normal : NORMAL;
     float4 texcoord : TEXCOORD0;
 };

      所以在顶点着色其中我们可以通过appdata_base.XXX这种方式来直接获取模型的顶点信息了.由于我们要返回一个输出结构所以我们28行先定义一个结构变量.然后分别为里面的子变量赋值,由于模型的原始顶点位置是在模型空间的,而我们输出结构中的顶点位置是投影空间的,所以我们要先将它进行一些列的空间变换,如果你看过我写的前言,那么应该知道先要进行"模型空间->世界空间"变换再进行"世界空间->摄像机空间"变换,最后进行”摄像机空间->投影空间”的变换.看起来很繁琐,不过Unity已经帮我们搞定了,Unity_Matrix_MVP就是这一系列变换综合起来的变换矩阵了,如果想了解矩阵相关知识,可以查阅线性代数的相关资料.所以我们只要把我们在模型空间的坐标乘以这个变换矩阵就可以了.矩阵相乘通过CG提供的函数mul();但是要记住mul(UNITY_MATRIX_MVP,input.vertex)和mul(input.vertex,UNITY_MATRIX_MVP)可是不同的,由于Unity是左手坐标系,我们必须使用前者将顶点位置右乘到变换矩阵才能得到正确结果,如果想知道其中原因同样查阅线性代数相关资料.那么通过o.pos = mul(UNITY_MATRIX_MVP,input.vertex)我们就完成对一个变量的赋值。再看看o.uv_MainTex = input.texcoord.xy;模型的纹理坐标最开始就是存在于TEXCOORD0中的,你一定那为什么还要用同样的语义赋值一遍呢,这个我也不是特别的清楚,我估计是只有通过顶点着色器返回后的结果才会被插值.所以需要这么写一遍,如果有哪位知道,请一定告诉我.由于我们在后面使用纹理坐标的时候只需要其中的前两位所以通过.xy的方式把它提取出来,这种.rgba或者.xyzw的语法是一种CG提供的特殊语法,因为CG中的变量大多是float3或者是风float4这种类型的所以提供这种快速提取子变量的方式提高效率.为什么我们把texcoord中的zw丢弃了呢,因为我么你用不到,至少在这个例子中我们只需要知道xy就可以在纹理贴图上找到相应的颜色了,为了减少计算量果断丢掉吧。现在完成了对输出结构的赋值我们返回它就可以了.
  第34~38行是片段着色器的代码,由于片段着色器的任务就是最后计算出一个颜色提供给渲染设备进行最后的处理,所以它的返回值为float4(存的是rgba).函数的名字与前面#pragma约定好的一致.参数的话就是刚刚顶点着色器输出结构.而最后面还跟了个:COLOR,这是为了把返回结果提供给:COLOR语义关联的寄存器,到时候渲染设备最后进行处理的时候去这里取片段着色器计算出来的结果就可以了.这里我们只用到了一个tex2D函数,这是一个根据纹理坐标查询2D贴图颜色的函数,第一个参数是贴图,就是我们在上面声明的_MainTex,写上去即可,而后面是我们要查询的坐标,也直接把顶点输出结构里我们赋值的纹理坐标填上去就可以了,最终将为我们返回一个贴图上的颜色.额为提一点,tex2D是不可以在顶点着色其中使用的,这是由于渲染管线的结构和目前显卡硬件设备的限制,但是在不久的将来这一点一定会被解决的.
  (~ o ~)~写到这里,本章节就快结束了,可以松口气了,不知不觉竟然写了3个小时.最后上几张图,大家看一下效果.这只是最基础的把贴图颜色加上去.
  如果你完全没使用过Shader,建议你看看官方文档,我这里大概描述一下:
  1.先新建一个Shader.然后把你要写的代码写进去.
  2.新建一个材质球,或者使用现有的材质球,在材质球上选择你所编写的Shader,如下图在Shader选项中选择,并设置相应的参数,我们这个例子中把你的贴图原图拖到右侧的Select处就可以了.
 

 
  3.把这个材质球,拖到你的模型上,或者修改一下你模型自带的材质球的Shader.
  最好看一下效果和两张用到的贴图,以让大家更好理解纹理坐标的意义
  

 
  上面这张是使用的默认的Shader “Diffuse"在没有贴图情况下的效果
     

 
  上面这张是使用了我们刚刚编写的Shader并加上了贴图
  下面是我们用到的两张贴图,当然这些贴图在制作游戏的过程中,都是美术提供的,如果你想做技美也可以自己学学.
  

 

 
  好了各位,这次的章节到此就结束了,如果有什么不理解的可以留言,或者我有什么写错的地方如果您发现了请及时指出.同时在这里也鼓励大家多多写博客与别人分享提高自己。谢谢~