本系列文章由Aimar_Johnny编写,欢迎转载,转载请标明出处,谢谢。
http://blog.csdn.net/lzhq1982/article/details/73442306
1、Unity Shader概述
1)在Unity中我们需要配合使用材质(material)和Unity Shader才能达到需要的效果。材质是载体,Unity Shader是文本文件,我们在Unity Shader中写好渲染的代码(属性,顶点着色器,片元着色器等),加载到材质上,在材质上可以调节shader的属性,然后将材质赋给模型展示渲染效果。
2)Unity Shader不是传统意义上的的Shader,Unity Shader实际上指的是一个ShaderLab文件--以.shader为文件后缀的一种文件。但Unity Shader可以做的远多于一个传统意义的Shader。
a、传统Shader中,我们只可以编写特定类型的Shader,比如顶点着色器,或片元着色器等。Unity Shader中我们可以在一个文件中编写他们。
b、传统Shader中,我们不能做一些例如开启混合、深度测试等渲染测试,这是在另外的代码中设置的。Unity Shader中我们加几个指令就可以了。
c、传统Shader中,需要编写大量代码设置着色器的输入和输出。Unity Shader中,我们只需要声明一些属性,并可以在材质中改变这些属性。对于模型数据(顶点位置,纹理坐标,法线,切线等),Unity Shader中我们可以直接访问,传统Shader需要自行编码传给shader。
d、Unity Shader的高度封装,使其失去了更多的编写自由性,不过同时也使我们只需要和Unity Shader打交道,不用关心渲染引擎底层实现。
3)Unity 5.5版本中,提供了5种Unity Shader模板:Standard Surface Shader,Standard Surface Shader(Instanced),Unlit Shader,Image Effect Shader, Compute Shader。
a、Standard Surface Shader是基于物理渲染(PBS)的包含标准光照模型的表面着色器模板,在5.x版本中,我们新建一个材质,它会默认使用内置的Standard Shader,是使用基于物理渲染(PBS)的技术,区别于传统光照Shader,它对光照渲染有着更加强大的支持,当然如果想达到强大的效果,除了主纹理和法线纹理,你可能需要加入金属纹理,粗糙度纹理,遮挡纹理,细节纹理,并配置一堆属性,再加上光照探针,反射探针啥的,想清楚再用。
b、Standard Surface Shader(Instanced),据说是5.4版本以后加的,使用了Gpu Instancing技术,这个技术在大量使用相同材质和网格的情况下可以大幅度降低Draw Call,而这个模板在生成阴影pass中使用了该技术。
c、Unlit Shader:不包含光照但包含雾效的顶点片元着色器。
d、Image Effect Shader:只是一个简单的图片展示效果,为实现屏幕后处理效果提供了基本模板。
f、Compute Shader:产生特殊的shader文件,旨在利用GPU的并行性来进行一些与常规渲染流水线无关的计算。(原书照搬,不在研究范围)。
a和b是表面着色器,c和d是顶点片元着色器,请对应选择模板。(因为喜欢用顶点片元着色器,我喜欢用d)
2、ShaderLab
上面提到过Unity Shader不是传统意义的shader,编写传统意义的shader需要和很多文件和配置打交道,Unity为我们提供了一层抽象:Unity Shader,而专门为Unity Shader服务的语言就是ShaderLab。
3、Unity Shader结构
1)名字
每个Unity Shader第一行都需要定义一个名字,格式例子:Shader "Custom/MyShader","/"是路径,其对应位置为Shader->Custom->MyShader。
2)属性(Properties)
Properties {
Name ("display name", PropertyType) = DefaultValue
Name ("display name", PropertyType) = DefaultValue
....
}
Properties中包含了一系列属性,会在材质面板中展示,开发者可以方便调试它们,同时shader会访问它们,类似于untiy的public变量。如上所示,每条属性由Name(名字,代码中要用),"display name"(面板显示名字),PropertyType(属性类型),DefaultValue(默认值)组成。Unity Shader属性类型如下:
3)SubShader
一个Shader文件可以包含多个SubShader,但最少要一个。Unity加载该shader时,会扫描并选择一个能在目标平台运行的SubShader,如果都不支持,会使用Fallback指定的Unity Shader。
SubShader通常定义如下:
SubShader {
//可选
[Tags]
//可选
[RenderSetup]
Pass {
}
//其他Pass
...
}
a、Tags:标签,由一组键值对组成,键和值都是字符串,它们告诉Unity渲染引擎,我希望怎样及何时渲染该SubShader。结构如下:
Tags { "TagName1" = "Value1" "TagName2" = "Value2"}
新手注意,每个标签间隔不要加逗号或分号,只是空格,最后也没符号,新手这里容易写错。
SubShader支持的标签类型如下(书上截图,原谅我懒):
注意,这里的标签是在SubShader里的,不是Pass里的,它们不同。
b、RenderSetup
状态设置,ShaderLab提供了一系列渲染状态的设置指令,可以设置在SubShader里,影响其下所有pass,也可以放某个pass里,只影响该pass。常见的渲染设置如下(接着截图):
c、Pass
SubShader中可以有多个pass,每个Pass定义了一次完整的渲染流程,但Pass过多会造成渲染性能下降,所以我们尽量用最少数目的Pass。
Pass包含的语义如下:
Pass {
[Name]
[Tags]
[RenderSetup]
//正式代码
....
}
Name "MyPassName"
通过该名字,我们可以在其他的Unity Shader中用UsePass命令直接使用该Pass,非常方便,例如
UsePass "MyShader/MYPASSNAME"
需要注意的是Unity内部会把所有Pass名称转换成大写字母,所以用UsePass时必须使用大写形式的名字。
上面SubShader的RenderSetup(状态设置)同样适用于Pass。
Pass的标签不同于SubShader的标签,下面是Pass中使用的标签:
还有一些特殊的Pass
UsePass:前面提到了,可以用Pass的名字复用访问。
GrabPass:该Pass负责抓取屏幕并存到一张纹理中,后面的Pass可以对该纹理继续处理。
d、FallBack
放在最后,当所有SubShader都不合适,就用FallBack提供的Shader。语义如下:
FallBack "name" 或 FallBack Off
不要以为它没用,当你学到阴影那里就知道它的用处了,FallBack的内置shader往往包含一个通用的阴影投射Pass,所以你不用自己实现。
4、Unity Shader种类
1)Surface Shader(表面着色器)
示例:
Shader "Custom/NewSurfaceShader" {
Properties {
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
}
SubShader {
Tags { "RenderType"="Opaque" }
CGPROGRAM
#pragma surface surf Lambert
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
fixed4 _Color;
void surf (Input IN, inout SurfaceOutputStandard o) {
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
Unity自己创造的一种着色器代码类型,少量的代码可以实现很大的功能,但其实它扩展开了就是顶点/片元着色器,只不过Unity替你封装好了很多代码,这样反而渲染代价比较大,你可以创建一个Surface Shader模板,看一下代码仅仅几行,然后可以在Inspector上点击show generated code,如下图:
然后你会看到它生成了一个庞大到让你想发狂的代码,立马就不想学了,哈哈,不过其实它就是生成了一个顶点/片元着色器的代码,就那几个模块,建议学完顶点/片元着色器再回头看看。不过写Surface Shader倒是很容易的,尤其是复杂的光照部分不用你管了,后面会有单独章节介绍。
2)Vertex/Fragment Shader(顶点/片元着色器)
示例:
Shader "Custom/VertexFragmentShader" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
}
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.uv;
return o;
}
sampler2D _MainTex;
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
我们主要学习的着色器,可以使用Cg/HLSL语言来编写,相对表面着色器复杂一些,但灵活性更高。
3)Fixed Function Shader(固定函数着色器)
示例:
Shader "Custom/FixedFunctionShader" {
Properties {
_Color ("Main Color", Color) = (1,1,1,1)
}
SubShader {
Pass {
Material {
Diffuse [_Color]
}
Lighting On
}
}
}
相对于上面两个可编程管线,固定管线技术已经落伍了,当然旧设备不支持可编程管线的要用固定函数着色器,而现在绝大多数GPU都支持可编程技术,而且Unity5.x目前所有固定函数着色器都会被编译成对应的顶点/片元着色器。
综上所述,虽然表面上Unity有三种着色器类型,但经过转化其实就只有一种:顶点/片元着色器,这也是我们后面着重学习的对象。
5、CG/HLSL,GLSL
相对于早期编写着色器的汇编语言,后来开发出了更高级的着色语言,常见的有DirectX的HLSL(High Level Shading Language),OpenGL的GLSL(OpenGL Shading Language),NVIDIA的CG(C for Graphic)。这些语言会被编译成汇编语言,也叫中间语言(Intermediate Language, IL)。中间语言再交给显卡驱动翻译成机器语言,即GPU可以理解的语言。
我们看到上面的Surface Shader和Vertex/Fragment Shader的代码示例,发现核心代码都放在CGPROGRAM和ENDCG之间,其实它们是在ShaderLab内部嵌套的CG/HLSL语言,因为CG和DX9风格的HLSL从写法上几乎是一样的,所以在Unity里CG和HLSL是等价的。我们也可以用GLSL来写,但发布平台会少一些,GLSL要嵌套在GLSLPROGRAM和ENDGLSL之间。
通常,Unity会自动把这些CG片段编译到所有相关平台上(D3D9,D3D11,OpenGL ES等),我们可以通过点击shader面板上的Compile and show code旁边的下拉按钮来看它当前编译的平台,你也可以更改要编译的平台。如下图:
但当发布游戏时,游戏数据文件中只包含目标平台需要的编译代码,其他平台的代码会被移除。