概述
材质和Unity Shader
在Unity 中我 需要配合使用 材质 (Material) 和 Unity Shader 能达到需要的效果。一个最常见的流程是
- 创建一个材质;
- 创建一 Unity Shader, 并把它赋给上一步中创建的材质
- 把材质赋给要渲染的对象;
- 在材质面板中调整 Unity Shader 属性 以得到满意的效果。
Unity Shader 定义了渲染所需的各种代码(如顶点着色器 片元着色器)属性(使用哪些纹理等)和指令(渲染和标签设置等),而材质允许我们调节这些属性,并将其最终赋给相应的模型
Unity中的Shader
Unity一共提供了4种Unity Shader模板供我们选择
- Standard Surface Shader 会产生一个包含了标准光照模型的表面着色器模板
- Unlit Shader 会产生一个不包含光照的基本顶点/片元着色器模板
- Image Effect Shader 为我们实现各种屏幕后处理效果提供了一个基本模板
- Compute Shader 会产生一种特殊shader文件,这类shader旨在利用GPU的并行性来进行一些与常规渲染流水线无关的计算
Standard Surface Shader为我们提供了典型的表面着色器的实现方法,但本书的重点在于如何在Unity中编写顶点/片元着色器,因此在后续的学习中,我们通常会使用Unlit Shader来生成一个基本的顶点/片元着色器模板。
ShaderLab
Unity为了解决自定义渲染需要很多文件和复杂的设置,提供了一层抽象———ShaderLab
ShaderLab是Unity提供的一种专门为Unity Shader服务的语言,是为Unity开发者提供的高层级的渲染抽象层
一个Unity Shader的基础结构:
Shader "ShaderName" {
Properties {
//属性
}
SubShader {
//显卡A使用的子着色器
}
SubShaderB {
//显卡B使用的子着色器
}
FallBack "VertexLit"
}
Unity Shader的结构
Name
每个Unity Shader文件的第一行都需要通过Shader语义来指定名字,可以通过在字符串中添加 / ,控制Shader在材质面板出现的位置
Shader "Custom/Shader" { }
Properities
Properties 语义块中包含了 一系列属性 (property),这些属性将会出现在材质面板中。
显示的名称 (display name) 则是出现在材质面板上的名字。我们需要为每个属性指定它的类型 (PropertyType)
Properties {
Name ("display name",PropertyType) = DefaultValue
Name ("display name",PropertyType) = DefaultValue
//更多属性
}
- 对于 Int Float Range 这些数字类型的屈性,其默认值就是一个单独的数字;
- 对于 Color 和 Vector 这类属性,默认值是用圆括号包围的一个四维向量;
- 对于 2D Cube 3D 种纹理类型,默认值的定义稍微复杂,它们的默认值是通过一个字符串后跟一个花括号来指定的
Shader "Unity Shaders Book/Chapter 3/MyShader" {
Properties {
// Numbers and Sliders
_Int ("Int", Int) = 2
_Float ("Float", Float) = 1.5
_Range("Range", Range(0.0, 5.0)) = 3.0
// Colors and Vectors
_Color ("Color", Color) = (1,1,1,1)
_Vector ("Vector", Vector) = (2, 3, 6, 1)
// Textures
_2D ("2D", 2D) = "" {}
_Cube ("Cube", Cube) = "white" {}
_3D ("3D", 3D) = "black" {}
}
}
SubShader
每一个Unity Shader文件可以包含多个SubShader语义块,但最少要有一个。
当Unity需要加载这个Unity Shader时,Unity会扫描所有的SubShader语义块, 然后选择第一个能够在目标平台上运行的SubShader。 如果都不支持的话, Unity就会使用Fallback语义指定的Unity Shader。
Unity提供这种语义的原因在于, 不同的显卡具有不同的能力。
SubShader{
//可选的
[Tags]
//可选的
[RenderSetup]
Pass {
}
//Other Passes
}
SubShader中定义了 一系列Pass以及可选的 状态([RenderSetup]) 和 标签([Tags]) 设置。 每个Pass定义了一次完整的渲染流程
状态和标签同样可以在Pass声明。不同的是,SubShader中的一些标签设置是特定的。 也就是说, 这些标签设置和Pass中使用的标签是不一样的。但是, 如果我们在SubShader进行了这些设置, 那么将会用于所有的Pass。
状态设置
Shader Lab提供了一 系列渲染状态的设置指令, 这些指令可以设置显卡的各种状态, 例如是
否开启混合/深度测试等。
当在SubShader块中设置了下述渲染状态时,将会应用到所有的Pass。
SubShader的标签
SubShader的标签(Tags)是一个键值对(Key/Value Pair), 它的键和值都是字符串类型。
这些键值对是SubShader和渲染引擎之间的沟通桥梁。它们用来告诉Unity的渲染引擎:我希望怎样以及何时渲染这个对象
Tags { "TagName1" = "Value1" "TagName2" = "Value2" }
需要注意的是,上述标签仅可以在 SubShader 声明 ,而不可以在 Pass 块中声明。 Pass块虽然也可以定义标签,但这些标签是不同于 SubShader 的标签类型。
Pass语义块
Pass {
[Name]
[Tags]
[RenderSetup]
//Other code
}
Pass 名称
Name "MyPassName"
通过这个名称,我们可以使用 ShaderLab UsePass 令来直接使用其他 Unity Shader 中的Pass 。
因为Unity内部会把所有Pass的名称转换成大写字母的表示,因此,在使用UsePass命令时必须使用大写形式的名字
UsePass "MyShader/MYPASSNAME"
Pass渲染状态
SubShader 的状态设置同样适用于 Pass 。除了上面提到的状态设置外,在 Pass 中我们还可以使用固定管线的着色器。Pass 同样可以设置标签 但它的标签不同于 SubShader 的标签。
除了上面普通的 Pass 定义外, Unity Shader 支持一些特殊的 Pass, 以便进行代码复用或实现更复杂的效果。
- UsePass: 如我们之前提到的一样,可以使用该命令来复用其他 Unity Shader 中的 Pass
- GrabPass: 负责抓取屏称并将结果存储在 张纹理中,以用于后续的 Pass 处理
Fallback
紧跟在各个 SubShader 语义块后面的,可以是一个 Fallback 指令。Fallback用于在所有Subshader都不适用的情况下使用
Fallback "name"
//或者
Fallback off
Fallback 还会影响阴影的投射。
Unity Shader的形式
尽管 Unity Shader 可以做的事情非常多(例如设置渲染状态等),但其最重要的任务还是指定各种着色器所需的代码。这些工作可以在SubShader中也可以在Pass中实现。
在Unity Shader中使用三种形式来编写
Shader "MyShader" {
Properties {
//所需的各种属性
}
SubShader {
//表面着色器 Surface Shader
//或者
//顶点/片元着色器 Vertex/Fragment Shader
//或者
//固定函数着色器 Fixed Function Shader
}
SubShader {
//和上一个类似
}
{
表面着色器
表面着色器 (Surface Shader) 是Unity自己创造的一种着色器代码类型。它需要的代码很少, Unity 在背后做了很多工作,但渲染的代价比较大。本质上与顶点/片元着色器一样。
我们可以理解成,表面着色器是 Unity 对顶点/片元着色器的更高一层的抽象。它存在的价值在于, Unity 为我们处理了很多光照细节,使得我们不需要再操心这些“烦人的事情”。
Shader "Custom/Simple Surface Shader" {
SubShader {
Tags {"RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert
struct Input {
float4 color : COLOR;
};
void surf(Input IN,inout SurfaceOut o) {
o.Albedo = 1;
}
ENDCG
}
Fallback "Diffuse"
}
表面着色器被定义在 SubShader 语义块(而非 Pass 语义块)中的CGPROGRAM ENDCG 之间
原因是,表而着色器不需要开发者关心使用多少个 Pass 、每个Pass 如何渲染等问题, 会在背后为我们做好这些事情
CGPROGRAM ENDCG 之间的代码是使用 CG/HLSL 编写的,也就是说,我们需要把CG/HLSL 语言嵌套在 ShaderLab 语言中
顶点/片元着色器
在Unity中我们可以使用 CG HLSL 语言来编写顶点/片元着色器 (Vertex/Fragment Shader),它们更加复杂,但灵活性也更高。
Shader "Custom/Soimple VertexFragment Shader" {
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
float4 vert(float4 v : POSITION) : SV_POSITION {
return mul (UNITY_MATRIX_MVP,v);
}
fixed frag() : SV_Target {
return fixed4(1.0,0.0,0.0,1.0);
}
EDNCG
}
}
}
和表面着色器类似 顶点/片元着色器的代码也需要定义在 CGPROGRAM ENDCG 之间,但不同的是 顶点/片元着色器是写在 Pass 语义块内,而非 SubShader 内的,原因是,我们需要自已定义每个 Pass 需要使用的 Shader 代码 。
固定函数着色器
上面两种 nity Shader 形式都使用了可编程管线。而对于一些较旧 的设备 (其 GPU 仅支持DirectX 7.0 OpenGL 1.5 OpenGL ES 1. 1), 例如 iPhone 3, 它们不支持可编程管线着色器,因此,这时候我们就需要使用 固定函数着色器 (Fixed Function Shader) 来完成渲染。这些着色器往往只可以完 一些非常简单效果。
选择Unity Shader形式
- 除非你有非常明确的需求必须要使用固定函数着色器,例如需要在非常旧的设备上运行你的游戏(这些设备非常少见),否则请使用可编程管线的着色器,即表面着色器或顶点/片元着色器。
- 如果你想和各种光源打交道,你可能更喜欢使用表面着色器 ,但需要小心它在移动平台的性能表现。
- 如果你需要使用的光照数目非常少,例如只有 个平行光,那么使用顶点/片元着色器是个更好的选择。
- 最重要的是,如果你有很多自定义的渲染效果,那么请选择顶点/片元着色器。
Unity Shader != 真正Shader
- Unity 里, Unity Shader 实际上指的就是 ShaderLab 文件——硬盘上以 .shader 作为文件后缀的一种文件。Unity Shader里,我们可以做的事情远多于一个传统意义上的Shader
- 在传统的 Shader 中,我们仅可以编写特定类型的 Shader, 例如顶点着色器、片元着色器等。而在 Unity Shader 中,我们可以在同 个文件里同时包含需要的顶点着色器和片元着色器代码。
- 在传统的 Shader 中,我们无法设置一些渲染设置,例如是否开启混合、深度测试等,这些是开发者在另外的代码中自行设置的。而在 Unity Shader 中,我们通过 行特定的指令就可以完成这些设置。
- 在传统的 Shader 中,我们需要编写冗长的代码来设置着色器的输入和输出,要小心地处理这些输入输出的位翌对应关系等。而在 Unity Shader 中,我们只需要在特定语句块中声明一些属性,就可以依靠材质来便地改变这些属性。而且对于模型自带的数据(如顶点位置、纹理坐标、法线等),Unity Shader 也提供了直接访问的方法,不需要开发者自行编码来传给着色器