声明:本篇为博主阅读冯乐乐所著《Unity Shader入门精要》所记录的笔记
原作者博客地址 https://me.csdn.net/candycat1992
如有不实请指正,如有侵权请联系本人删除。
一、Unity Shader 概述
1.材质和Unity Shader
Unity中需要材质(material)和Unity Shader 才能达到需要的效果,一个最常见的流程是:
(1)创建一个材质;
(2)创建一个Shader,并把他赋给上一步创建的材质;
(3)把材质赋给要渲染的对象;
(4)在材质面板中调整Unity Shader的属性。
Shader 定义了渲染所需的各种代码(如定点着色器和片元着色器)、属性(如使用哪些纹理等)和指令(渲染和标签设置等),而材质则允许我们调节这些属性,并将其最终赋给相应模型。
2.Unity中的材质
Unity中的材质需要结合一个GameObject的Mesh或者Particle System组件来工作。
创建流程跟其他的类似。
3.Unity中的Shader
为了和通用的shader做语义区分,我们把Unity的shader文件统称为Unity Shader ,这是因为Unity Shader和渲染管线的shader有很大不同。
Unity Shader默认几种:
1.Standard Surface Shader : 一个包含了标准光照模型的Shader
2.Unlit Shader : 一个不包含光照、但包含雾效的基本顶点/片元着色器
3.Image Effect Shader :各种屏幕后处理效果的基本模版
4.Compute Shader:一种特殊的Shader,主要功能是利用GPU的并行性来进行一些与常规渲染流水线无关的计算。
Unity Shader 本质上是一个文本文件,和很多Unity的外部文件类似,Unity Shader 也有导入设置(Image Setting)面板,可以对其进行配置。
二、Unity Shader 的基础 ShaderLab
“计算机科学中的任何问题都可以通过增加一层抽象来解决”——大卫 惠勒
Unity为我们shader的学习提供了一层抽象 Unity Shader, 而ShaderLab就是其服务语言。
什么是ShaderLab
ShaderLab是Unity为开发者听的高层级的渲染抽象层,方便开发者控制。
ShaderLab是编写Unity Shader的说明性语言。
使用一些嵌套在花括号内部的语义(Synatx)来描述文件的结构。
其中Properties中包含着色器所需的各种属性,这些属性将会出现在材质面板中。
定义了要显示一个材质所需要的所有东西,而不仅仅是着色器代码。
一个Unity Shader的基本结构如下所示
Shader "ShaderName" {
Propertites{
//属性
}
SubShader{
//显卡A使用的字着色器
}
SubShader{
//显卡B使用的字着色器
}
FallBack "VertexLit";
}
Unity会在后面根据平台翻译成真正的代码和Shader文件,而开发者只需要和ShaderLab打交道即可。
三、Unity Shader的结构
1. 给Shader起个名字
每个Unity Shader第一行都需要同个shader 语义来指定该Unity Shader的名字。这个名字由一个字符串来定义,例如“MyShader”。
Shader "Custom/MyShader" { }
2.材质和Unity Shader的桥梁(Property)
Property与一块中包含了一系列属性(property)
Properties{
Name( "display name", PropertyType ) = DefaultValue;
Name( "display name", PropertyType ) = DefaultValue;
//更多属性
}
开发者们声明这些属性是为了在材质面板中能够方便地调整各种材质属性。如果我们需要在Shader中访问他们,就需要使用每个属性的名字(Name)。
在Unity中,这些属性的名字通常由一个下划线开始。
显示的名称(display name)则是出现在面板上的名字。
需要为每个属性指定它的类型(PropertType)常见属性如下表
属性类型 | 默认的定义语法 | 例子 |
---|---|---|
Int | numbel | _Int(“Int”,Int)=2 |
Float | numbel | _Float ("Float ",Float )=1.5 |
Range(min,max) | numbel | _Range(“Range”,Range(0.0,6.0))=2.0 |
Color | (number,number,number,number) | _Color(“Color”,Color)=(1,1,1,1) |
Vector | (number,number,number,number) | _Vector(“Vector”,Vector)=(2,3,4,1) |
2D | “defaulttexture”{} | _2D(“2D”,2D)="" {}; |
Cube | “defaulttexture”{} | _Cube(“Cube”,Cube)=“white” {}; |
3D | “defaulttexture”{} | _3D(“3D”,3D)=“black” {}; |
对于Int、Float、Range这些数字类型属性,默认值为单独数字
对于Color、Vector这类属性,默认值为四维向量
对于2D、Cube、3D这三种纹理类默认值的定义稍微复杂,默认值为一个字符串后面跟一个花括号来指定的,其中,字符串要么是空的,要么是内置的纹理名称如“white”、“black”、“gray”、“bump”。花括号的用处原本是指定纹理属性 。
3.SubShader
每个Unity Shader 文件可以包含多个SubShader 语义块,但最少要有一个。其意义在于当Unity需要加载这个UnityShader时会扫描所有的SubShader语义块,然后选择第一个。如果都不支持的话,Unity就会使用FallBack语义指定的UnityShader。可以根据显卡性能区别配置。
SubShader {
//可选的
[Tags]
//可选的
[RenderSetup]
Pass{
}
//Other Passes
}
SubShader中定义了一系列Pass以及可选的状态([RenderSetup])和标签([Tags])设置。
每个pass定义了一次完整的渲染流程,但如果pass数量过多,往往会造成渲染性能下降。因此我们应该尽力那个使用最小数目的pass。
状态和标签同样是在pass声明,不同的是,SubShader中的一些标签设定是特定的。也就是说,这些标设置和pass中使用的标签是不一样的。而对于状态设置来说,其使用的语法是相同的。但是,如果我们在SubShader进行了这些设置,那么将会用于所有的pass。
状态设置
状态名称 | 设置指令 | 解释 |
---|---|---|
Cull | Cull Back/Front/Off | 设置剔除模式:剔除背面/正面/关闭剔除 |
ZTest | ZTest Less Greater/LEqual/GEqual/Equal/NotEqual/Always | 设置深度测试时使用的函数 |
ZWrite | ZWrite On/Off | 开启/关闭深度写入 |
Blend | Blend SrcFactor DstFactor | 开启并设置混合模式 |
当在SubShader中设置了上述渲染状态时,将会应用到所有的Pass。如果我们不想这样(例如在双面渲染中我们希望在第一个Pass中剔除正面来对背面进行渲染,在第二个Pass中剔除备案来对正面进行渲染),可以在Pass语义块中单独进行上面的设置。
SubShader的标签
SubShader的标签(Tags)是一个键值对(Key/ValuePair),它的键和值都是字符串类型。这些键值对是subShader和渲染引擎之间的沟通桥梁。他们用来告诉Unity的渲染引擎:SubShader,我需要怎样和何时渲染这个对象
标签的结构如下:
Tags { "TagName1" = "Value1" "TagName2" = "Value2" }
标签类型 | 说明 | 例子 |
---|---|---|
Queue | 控制渲染顺序,指定该物体属于哪一个渲染队列,通过这种方式可以保证所有的透明物体可以在所有不透明物体后面呗渲染,我们也可以自定义使用的渲染队列来控制物体的渲染顺序 | Tags { “Queue” = “Transparent” } |
RenderType | 对着色器进行分类,例如这是一个不透明的着色器,或是一个透明的着色器等,这可以呗用于着色器的替换(Shader ReoIacement )功能。 | Tags { “RenderType” = “Qpaque” } |
DisableBatching | 一些SubShader在使用Unity的批处理功能时会出现问题,例如使用了模型空间下的坐标来进行顶点动画。这时可以通过该标签来指明是否对该SubShader使用批处理 | Tags { “DisableBatching”=“True” } |
ForceNoShadowCasting | 控制使用该SubShader的物体是否会投射投影 | Tags { “ForceNoShadowCasting” = “True” } |
IgnoreProjector | 如果该标签值为“True” 那么使用该SubShader的物体将不会受Project的影响,通常适用于半透明物体 | Tags { “IgnoreProjector” = “True” } |
CanUseSpriteAtlas | 当该SubShader是用于精灵(Sprites)将该标签设置为“False” | Tags { “CanUseSpriteAtlas” = “False” } |
PreviewType | 致命材质面板将如何预览该材质。默认情况下,材质将显示为一个球型,我们可以通过该标签把值设置为“Plane”、“SkyBox”来改变预览类型。 | Tags{“PreviewType” = “Plane”} |
注意:上述标签仅可在SubShader中声明,而不可在pass中声明。
pass中虽然也可定义标签但是与SubShader的不同。
Pass语义块
pass语义块包含的语义如下
Pass {
[Name]
[Tags]
[RenderSetup]
//Other Code
}
首先,我们可以在Pass中定义该Pass的名称:
Name "MyPassName"
通过这个名称,我们可以使用ShaderLab的UsePass命令来直接使用其他UnityShader中的pass 例如:
UsePass “MyShader/MYPASSNAME”
这样可以提高代码的复用性,需要注意的是,由于Unity内部会把所有Pass的名称转换成大写字母的表示,影刺在使用UsePass命令时,必须使用大写形式的名字。
其次,我们可以对pass设置渲染状态。
SubShader的状态设置同样适用于Pass。除了上面的设置状态外,在pass中我们还可以使用固定管线的着色器命令。
pass同样可以设置标签,是告诉渲染引擎我们希望怎样渲染物体
标签类型 | 说明 | 例子 |
---|---|---|
LightMode | 定义该Pass在Unity的渲染流水线中的角色 | Tags{ “LightMode” = “ForwardBase” } |
RequireOtions | 用于指定当满足某些条件时材渲染该Pass,它的值时一个由空格分隔的字符串。到5.x版本,Unity支持的此选项有:SoftVegetation | Tags {“REquineOptions”=“SoftVegetaion”} |
除了上面普通的Pass定义外,UnityShader还支持一些特殊的Pass,以便进行代码复用或者实现更复杂的效果
UsePass:如我们之前提到过的一样,可以使用该命令来复用其他UnityShader中的pass;
RequireOptions: 该Pass负责抓取屏幕并将结果存储在一张纹理中,以用于后续的Pass处理
留一条后路:FallBack
如果其他SubShader不可用,就使用这个
FallBack “name” (都不可用就去调用最低级的)
//或者
FallBack off (不再管了= = )
4.UnityShader的形式
在Unity中我们可以使用下面三种形式来编写UnityShader ,而不管哪种形式,真正意义上的Shader代码都需要包含在ShaderLab中:
Shader "MyShader"{
Properties{
//所需的各种属性
}
SubShader{
//真正意义上的SHader代码会出现在这里
//表面着色器(Surface Shader)或者
//顶点/片元着色器(Vertex/Fragment Shader)或者
//固定函数着色器(Fixed Function Shader)
}
SubShader{
//和上一个类似
}
}
表面着色器(SurfaceShader)
Unity自己创建的着色器代码类型。它所需要的代码量较少,但渲染代价比较达。他在本质上和下面要讲到的顶点/片元着色器是一样的。也就是说,Unity在使用时仍需要将他转化成顶点/片元着色器。
Shdaer "Custom/Simple Surface Shader"{
SubShader{
Tags{ "RenderType" = "Qpaque" }
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float4 color : COLOR ;
};
void surf ( Input IN , inout SurfaceOutput o ){
o.Albedo = 1;
}
ENDCG
}
Fallback "Diffuse";
}
从上述程序可以看出,表面着色器被定义在SubShader语义块(而非 Pass语义块)中的CGPROGRAM和ENDCG之间。原因是,表面着色器不需要开发者关心使用多少个Pass、Pass如何渲染等问题,Unity会在背后为我们做好这些事情,我们需要做的是告诉他,用这些纹理去填充颜色,用这些法线纹理去填充发现,使用Lambert光照模型,其他的自行处理。
CGPROGRAM和ENDCG之间的代码是使用CG/HLSL编写的,也就是说,我们需要把CG/HLSL签到在ShaderLab语言中。值得注意的是 这里的CG/HLSL是Unity经封装后提供的,语法虽然几乎一样,但还是有些许不同,有些原生函数和用法Unity并没有支持。
顶点/片元着色器(Vertex/Fragment Shader)
同样使用CG/HSLS语言编写顶点/片元着色器,这里更复杂,但灵活性也更高。
Shader "Custom/Simple VertexFragment Shader"{
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 v : POSITION) : SV_POSITION{
return fixed4(1.0,0.0,0.0,1.0);
}
ENDCG
}
}
}
和表面着色器不同的是这里的代码是写在Pass中 而非SubShader内的。原因是我们需要自给定义每个Pass需要的Shader代码。虽然我们可能需要编写更多的代码,但带来的好处是灵活性很高。更重要的是,我们可以控制渲染的实现细节。
固定函数着色器(Fixed Function Shader)
不想看,好像是dx7的东西,网上讨论他们还是05年的事。
选择哪种UnityShader形式
1.除非有明确需求要用固定函数着色器,否则尽量使用可编程的
2.如果和光源打交道,表面着色器更合适,但要注意在移动平台的性能表现。
3.如果使用的光线非常少,尽量使用顶点/片元着色器。
4.最重要的是,如果有很多自定义的渲染效果,那么请使用顶点/片元着色器。
四、其他
1.UnityShader != Shader
UnityShader实际上指的是一个shaderLab文件,后缀是.shader
在UnityShader中我们可做的事情远多于一个传统意义上的shader。
(1)传统shader一个文件只能编写一种类型,而UnityShader可以在一个文件中包含定点着色器和片元着色器所需的代码。
(2)传统shader无法设置一些渲染设置,例如是否u开启混合、深度测试等,这些是开发者在另外的代码中自行设置的,而UnityShader只需要一行特定代码。
(3)传统Shader需要冗长的代码来控制输入输出。而UnityShader只需在特定语句块中声明属性,就可以依靠材质来改变。而且对于模型自带的数据(如定点位置、纹理坐标、法线等),UnityShader提供了直接访问的方法,不需要开发者自行编码来传递。
而UnityShader由于高度封装性,我们可编写的类型和语法被限制了,对于一些类型的shader,例如曲面细分着色器(Tessellation Shader)、几何着色器(Geometry Shader)等,UnityShader的支持行就差一些。
2.UnityShader和CG/HLSL的关系
UnityShader是有ShaderLab语言编写的,但对于表面着色器和顶点/片段着色器我们可以在ShaderLab中嵌套CG/HSLG进行编写。
由于表面着色器最终被Unity转化为顶点/片元着色器,所以本质上来说UnityShader只有两种形式:顶点/片元着色器。
可以在Shader倒入设置面板中点击 Show generated code 查看Untiy转化来的真正的顶点/片元着色器代码。