Unity Shader分为表面着色器(Surface Shader),顶点着色器(Vertex Shader),片段着色器(Fragment Shader)
我们先来看个简单的Surface Shader,看看它都能做什么。
首先我们通过"Create->3D Object ->Plane"创建一个面板
然后通过"Create->Shader->Standard Surface Shader"创建一个表面着色器,并修改名字为SurfaceShader
然后通过"Create->Material"创建一个材质,并修改名字为MaterialSurfaceShader,材质使用的shader指定为我们刚创建的SurfaceShader
然后我们把材质MaterialSurfaceShader拖到面板Plane上
我们用MonoDevelop打开刚创建的表面着色器SurfaceShader,看看里面都有什么
Shader "Custom/SurfaceShader" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
void surf (Input IN, inout SurfaceOutputStandard o) {
// Albedo comes from a texture tinted by color
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
我们一行一行的对这段代码进行解析,解析完后,我们就可以读懂一些简单的unity shader了
第一行
Shader "Custom/SurfaceShader" { }
Shader是关键字,标明这个文件是个Shader,跟在后面的"Custom/SurfaceShader" 是个路径,我们在给材质选择Shader的时候会找到这个路径
第二行到第七行是属性区域
第二行
Properties{ }
Properties是属性的意思,和C#中的属性是一个意思,我们在这里定义一些属性,以便我们在后面用。
我们定义的这些属性在unity的Inspector面板中都能看到
我们来看看默认都有那些属性:
第三行
_Color ("Color", Color) = (1,1,1,1)
针对这句话的讲解我找到了一张图片
这里的内部名称 就是属性的名称,也是我们在后面用到该属性的时候用的名字
编辑器中显示的名称 就是在unity的Inspector面板中显示的名称
属性类型 标明该属性的类型,就像我们在c#中声明一个整形的变量a,我们会这样写 private int a,这里的int就是用来标明a是一个整形。同样Color标明我们的属性_Color是一个颜色
默认值 就是属性的默认值,针对颜色的这个,分别是r,g,b,a
我们来看看第三行的属性我们会在unity的Inspector面板中看到什么
我们把这个颜色变成红色,然后我们看看我们的面板发生了什么
我们的面板变成红色了!!
我们可以通过这种方式改变我们材质的颜色
第四行
_MainTex ("Albedo (RGB)", 2D) = "white" {}
2D的意思是2D纹理,关于表示纹理的属性,unity文档中有下面三种
我们来看看第四行会在unity的Inspector面板中显示什么
我们通过"Albedo (RGB)"区域的右边Texture中的select选择一张我们的纹理图片看看是什么效果。我们需要把颜色还原为白色
我们的面板Plane有图片了!!!
可以看到第四行的作用是用来设置纹理图片的,我们可以通过属性的名字_MainTex来操作这幅纹理图片
第五行和第六行分别设置平滑程度(Smoothness)和金属性(Metallic)的,这两个属性的作用在前一章中已经讲过,这里不再重复讲,我们来看看在unity的Inspector面板中会出现什么
第三行到第39行驶着色器执行的区域SubShader,也就是在这个区域中会用到我们在属性区域中定义的属性
如果我们定义的属性在SubShader没有使用,那么这个属性将不会有任何效果
第八行
SubShader{ }
SubShader是个关键字,表示着色器执行的区域
第九行
Tags { "RenderType"="Opaque" }
Tags是个关键字,表示标签的意思,作用是告诉引起这个物体什么时候已经怎样被渲染出来
下面是unity文档中对tags的说明及语法格式
unity shader的标签有很多,我们列举出常用的几个,其他的我怕们可以在unity文档中找到
那么第九行的意思就是渲染不透明的物体
第十行
LOD 200
Unity中的LOD表示着色器细节等级,LOD 200表示只有在细节等级小于等于200时才起作用,细节等级如下图
第十二行到第十八行驶CG程序,CG程序以CGPROGRAM开始,以ENDCG结束
第十二行
CGPROGRAM 表示CG程序的开始
第十四行
#pragma surface surf Standard fullforwardshadows
针对这句话的解释看下图
所以 #pragma是一个关键字,用来声明一些东西的
surface 表示声明一个表面着色器
surf是我们表面着色器函数的名字,这个函数在第29行
Standard fullforwardshadows 是一个光照模型,unity中的光照模型分为Lambert,Phone等,使用不同的光照模型,会产生不同的效果
第十七行
#pragma target 3.0
表示把shader编译为3.0版本,不同的版本支持的性能会有不同
第十九行
sampler2D _MainTex;
声明我们将要用到的属性,我们在属性区域中定义了一些属性,想要用着这些属性,必须在SubShader区域再次声明一个名字一样的字段,才可以使用。
sampler2D是个啥?其实在CG中,sampler2D就是和texture所绑定的一个数据容器接口。等等..这个说法还是太复杂了,简单理解的话,所谓加载以后的texture(贴图)说白了不过是一块内存存储的,使用了RGB(也许还有A)通道,且每个通道8bits的数据。而具体地想知道像素与坐标的对应关系,以及获取这些数据,我们总不能一次一次去自己计算内存地址或者偏移,因此可以通过sampler2D来对贴图进行操作。更简单地理解,sampler2D就是GLSL中的2D贴图的类型,相应的,还有sampler1D,sampler3D,samplerCube等等格式。
解释通了sampler2D是什么之后,还需要解释下为什么在这里需要一句对_MainTex的声明,之前我们不是已经在Properties里声明过它是贴图了么。答案是我们用来实例的这个shader其实是由两个相对独立的块组成的,外层的属性声明,回滚等等是Unity可以直接使用和编译的ShaderLab;而现在我们是在CGPROGRAM...ENDCG这样一个代码块中,这是一段CG程序。对于这段CG程序,要想访问在Properties中所定义的变量的话,必须使用和之前变量相同的名字进行声明。于是其实sampler2D _MainTex;做的事情就是再次声明并链接了_MainTex,使得接下来的CG程序能够使用这个变量。
第二十一到二十三行
struct Input {
float2 uv_MainTex;
};
我们定义的一个结构体,作为表面着色器函数surf的输入参数
第二十五到第二十七行
half _Glossiness;
half _Metallic;
fixed4 _Color;
作用和第十九行类似
第二十九到第三十七行是我们的表面着色器函数surf
上面的#pragma段已经指出了我们的着色器代码的方法的名字叫做surf,着色器就是给定了输入,然后给出输出进行着色的代码。CG规定了声明为表面着色器的方法(就是我们这里的surf)的参数类型和名字,因此我们没有权利决定surf的输入输出参数的类型,只能按照规定写。这个规定就是第一个参数是一个Input结构,第二个参数是一个inout的SurfaceOutput结构。
它们分别是什么呢?Input其实是需要我们去定义的结构,这给我们提供了一个机会,可以把所需要参与计算的数据都放到这个Input结构中,传入surf函数使用;SurfaceOutput是已经定义好了里面类型输出结构,但是一开始的时候内容暂时是空白的,我们需要向里面填写输出,这样就可以完成着色了。先仔细看看INPUT吧,现在可以跳回来看上面定义的INPUT结构体了:
struct Input {
float2 uv_MainTex;
};
作为输入的结构体必须命名为Input,这个结构体中定义了一个float2的变量…你没看错我也没打错,就是float2,表示浮点数的float后面紧跟一个数字2,这又是什么意思呢?其实没什么魔法,float和vec都可以在之后加入一个2到4的数字,来表示被打包在一起的2到4个同类型数。
在访问这些值时,我们即可以只使用名称来获得整组值,也可以使用下标的方式(比如.xyzw,.rgba或它们的部分比如.x等等)来获得某个值。在这个例子里,我们声明了一个叫做uv_MainTex的包含两个浮点数的变量。
如果你对3D开发稍有耳闻的话,一定不会对uv这两个字母感到陌生。UV mapping的作用是将一个2D贴图上的点按照一定规则映射到3D模型上,是3D渲染中最常见的一种顶点处理手段。在CG程序中,我们有这样的约定,在一个贴图变量(在我们例子中是_MainTex)之前加上uv两个字母,就代表提取它的uv值(其实就是两个代表贴图上点的二维坐标 )。我们之后就可以在surf程序中直接通过访问uv_MainTex来取得这张贴图当前需要计算的点的坐标值了。
我们来看看struct Input中我们可以定义的默认值有哪些,下面的内容来自unity文档
Surface Shader input structure
The input structure Input
generally has any texture coordinates needed by the shader. Texture coordinates must be named “uv
” followed by texture name (or start it with “uv2
” to use second texture coordinate set).
Additional values that can be put into Input structure:
-
float3 viewDir
- will contain view direction, for computing Parallax effects, rim lighting etc. -
float4
withCOLOR
semantic - will contain interpolated per-vertex color. -
float4 screenPos
- will contain screen space position for reflection or screenspace effects. -
float3 worldPos
- will contain world space position. -
float3 worldRefl
- will contain world reflection vector if surface shader does not write to o.Normal. See Reflect-Diffuse shader for example. -
float3 worldNormal
- will contain world normal vector if surface shader does not write to o.Normal. -
float3 worldRefl; INTERNAL_DATA
- will contain world reflection vector if surface shader writes to o.Normal. To get the reflection vector based on per-pixel normal map, useWorldReflectionVector (IN, o.Normal)
. See Reflect-Bumped shader for example. -
float3 worldNormal; INTERNAL_DATA
- will contain world normal vector if surface shader writes to o.Normal. To get the normal vector based on per-pixel normal map, useWorldNormalVector (IN, o.Normal)
如果你坚持看到这里了,那要恭喜你,因为离最后成功读完一个Shader只有一步之遥。我们回到surf函数,它的两有参数,第一个是Input,我们已经明白了:在计算输出时Shader会多次调用surf函数,每次给入一个贴图上的点坐标,来计算输出。第二个参数是一个可写的SurfaceOutput,SurfaceOutput是预定义的输出结构,我们的surf函数的目标就是根据输入把这个输出结构填上。SurfaceOutput结构体的定义如下
struct SurfaceOutput {
half3 Albedo; //像素的颜色
half3 Normal; //像素的法向值
half3 Emission; //像素的发散颜色
half Specular; //像素的镜面高光
half Gloss; //像素的发光强度
half Alpha; //像素的透明度
};
这里的half和我们常见float与double类似,都表示浮点数,只不过精度不一样。也许你很熟悉单精度浮点数(float或者single)和双精度浮点数(double),这里的half指的是半精度浮点数,精度最低,运算性能相对比高精度浮点数高一些,因此被大量使用.
第三十一行
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
这里用到了一个tex2d函数,这是CG程序中用来在一张贴图中对一个点进行采样的方法,返回一个float4。这里对_MainTex在输入点上进行了采样,然后再乘与颜色值
第三十二行
o.Albedo = c.rgb;
将其颜色的rbg值赋予了输出的像素颜色
第三十四行
o.Metallic = _Metallic
将其金属性值赋予了输出的像素的金属性值
第三十五行
o.Smoothness = _Glossiness;
将其平滑性值赋予了输出的像素的平滑性值
第三十六行
o.Alpha = c.a;
将其alpha值赋予了输出的像素的alpha
第三十八行
ENDCG
ENDCG 是个关键字,标明CG程序的结束
第四十行
FallBack "Diffuse"
如果说我们的SubShader由于某些原因没有被执行,那就使用系统默认的shader
OVER