Unity Shader基础(一)

Unity Shader基础知识的学习记录。

unity中的shader

Unity中的shader用来定义与渲染相关的各种代码(如顶点和片元着色器)、属性(如使用哪些纹理)和指令(渲染和标签设置等)。在使用shader时需要结合材质(Material)。通过材质我们可以调整shader的属性并将其赋给要渲染的模型。

传统的shader

Unity中shader和传统图形学shader不同之处

传统shader Unity shader
只能编写特定类型的shader 如顶点或片元着色器 可以同时在同一个文件里包含所需的顶点和片元着色器代码
无法改变渲染设置 可以通过特定指令开启混合、深度测试等
编写冗长的代码来设置着色器的输入和输出,并保证其位置对应关系 在特定语句块中声明属性,即可通过材质来改变这些属性,对于模型自身的属性Unity shader也可以直接访问
由于高度的封装,可编写的shader代码也会受到一些限制

分类

在Unity中,当创建shader时,会有以下四种选择:

  • Standard Surface Shader

基于物理的渲染(PBR),包含了标准光照模型的表面着色器。

  • Unlit Shader

不包含光照的最基本的顶点/片元着色器。

  • Image Effect Shader

用于实现屏幕后处理效果的模板。

  • Compute Shader

与渲染无关,利用GPU进行并行计算时使用。

ShaderLab

ShaderLab是Unity提供的用来编写Unity shader的一种说明性语言。它使用一些嵌套在花括号内的语义(Syntax)来描述一个Unity shader文件的结构。
下边用一个简单的Unity shader来介绍Unity shader的结构及ShaderLab的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Shader "Custom/NewSurfaceShader" {
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"
}

其中包含以下一些元素:

shader名称

Unity shader的第一行用来为其指定名字。上述shader定义为"Custom/NewSurfaceShader",表示位于Custom路径下,名为NewSurfaceShader

Properties

Properties块包含了一系列的属性,也是Unity shader和材质交流的途径。Properties块内定义方式一般为:

1
Name("Display Name", PropertyType) = defaultValue

其中各参数含义如下:

标记 描述
Name 属性名,通常定义为以一个下划线开头
Display Name 该属性出现在材质的检视面板上时显示的名称
PropertyType 属性类型,可选的类型及默认值等见下表
PropertyType 属性类型,可选的类型及默认值的形式等见下表
defaultValue 属性的默认值

各属性类型及默认值形式表:

类型 默认值格式 示例
Int number _Int (“Int”, Int) = 2
Float number _Float (“Float”, Float) = 1.5
Range(min, max) number _Range(“Range”, Range(0.0,5.0)) = 3.1
Color (number,number,number,number) _Color (“Color”, Color) =(1,1,1,1)
Vector (number,number,number,number) _Vector (“Vector”, Vector) = (2,3,6, 1)
2D “defaulttexture” {} _2D (“2D”, 2D) = “” {}
Cube “defaulttexture” {} _Cube (“Cube”, Cube) “white” {}
3D “defaulttexture” {} _3D (“3D”, 3D) “black” {}

注意2D、Cube、3D这三种类型,默认值是由一个字符串和一个花括号来定义的。字符串可以是空字符串或者内置纹理的名称。花括号在旧的Unity版本中用来指定一些纹理属性,新版本(5.0以后)中这些纹理属性则需要在顶点着色器中编写。

在Properties中声明这些属性的主要目的是为了在Inspector上显示出这些属性。即使没有在Properties中声明,CG代码片中也可以直接定义变量。

SubShader

每个Unity shader中都必须包括至少一个SubShader。Unity加载此Unity shader时,会扫描所有的SubShader,然后选择第一个能在该目标平台执行的SubShader,如果都不支持的话就选择Fallback中指定的Unity shader。
SubShader的结构通常如下,包含了一些可选的标签Tags、状态设置CommonState以及一个或多个Pass:

1
2
3
4
5
6
7
8
9
10
11
Subshader {

[Tags]

[CommonState]

Passdef

[Passdef ...]

}

Tag

标签是一组键值对,键和值都是字符串,这些键值对是SubShader和渲染引擎之间沟通的桥梁。
SubShader支持的标签如下表:

标签类型 说明 例子
Queue 控制渲染顺序,指定该物体属于哪一个渲染队列,通过这种方式可以控制所有的透明物体在非透明物体之后渲染,也可以自定义使用的渲染队列来控制物体的渲染顺序 Tag { “Queue” = “Transparent” }
RenderType 对着色器进行分类,例如这是一个不透明的着色器,或者是一个透明的着色器等 Tag { “RenderType” = “Opaque” }
DisableBatching 一些SubShader在使用Unity的批处理功能时会出现问题,例如使用了模型空间下的坐标进行顶点动画时等。这时可以通过该标签来指明是否对该SubShader使用批处理 Tag { “DisableBatching” = “True” }
ForceNoShadowCasting 控制使用该SubShader的物体是否会投射阴影 Tag { “ForceNoShadowCasting” = “True” }
IgnoreProjector 如果该标签值为”True”,那么使用该SubShader的物体将不受Projector的影响,通常用于半透明物体 Tag { “IgnoreProjector” = “True” }
CanUseSpriteAtlas 当该SubShader是用于精灵Sprite时,将该标签设置为”False” Tag { “CanUseSpriteAtlas” = “False” }
PreviewType 指明材质面板将如何预览该材质,默认为球形,可以改为”Plane”、”SkyBox”等 Tag { “PreviewType” = “Plane” }

CommonState

SubShader中可以定义一组渲染状态(Render State),通常是一些渲染的开关选项,他们对整个SubShader内所有的Pass都有效,所以称CommonState。这些渲染状态也可以在每个Pass中分别定义,则只对每个Pass起效。

渲染状态的指令可以指定显卡的各种状态,常见的一些渲染状态设置选项见下表:

状态 指令 描述
Cull Cull Back | Front | Off 设置剔除模式
ZTest ZTest Less | Greater | Equal | LEqual | GEqual | NotEqual | Always 设置深度测试使用的函数
ZWrite ZWrite On | Off 深度写入
Blend Blend SrcFactor DstFactor 开启并设置混合模式

Pass

Pass分为以下三种:

常规Pass

Pass的语法通常如下:

1
2
3
4
5
6
Pass { 
[Name]
[Tags]
[RenderSetup]
// other code
}

首先是定义Pass的名称,如:

1
Name "MyPassName"

有了这个名称就可以使用ShaderLab的UsePass命令来直接使用其它的Unity Shader中的Pass,如:

1
UsePass "MyShader/MYPASSNAME"

这样大大提高了代码的复用性,注意Unity内部会把所有的Pass名称转化为大写字母形式,因此在使用UsePass时必须使用大写形式的名字。

名字之后是渲染状态的设置。SubShader中的渲染状态设置同样适用于Pass。除了上边提到的状态设置之外,Pass中还可以使用固定管线的着色器命令。

Pass中同样可以设置标签,也是用来告知渲染引擎我们希望怎样来渲染物体。但是此处的标签与SubShader中使用的标签不同。以下是Pass中使用的标签类型:

标签 说明 例子
LightMode 定义该Pass在Unity的渲染流水线中的角色 Tags {“LightMode” = “ForwardBase”}
RequiredOptions 用于指定当满足某些条件时才渲染该Pass,它的值是一个由空格分隔的字符串。目前,Unity支持的选项有:SoftVegetation。在后边的版本中可能会增加更多的选项 Tags {“RequiredOptions” = “SoftVegetation”}
Use Pass

可以使用该命令来复用其他UnityShader中的Pass。

Grab Pass

该Pass负责抓取屏幕并将结果存储在一张纹理中,以用于后续的Pass处理。

着色器代码

CGPROGRAMENDCG包围起来的,使用嵌套在ShaderLab语言中CG/HLSL编写的着色器代码。这些代码和标准的CG/HLSL语法几乎一样。可以用于编写Unity表面着色器或顶点/片元着色器,后边详细介绍。

Fallback

Fallback指令紧跟在各个SubShader语义块后边,当上边所有的SubShader在这块显卡上都不能运行,那么就使用这个Fallback的shader,例如:

1
Fallback "VertexLit"

编写Unity shader

当在Unity中编写和使用Unity shader时可以有三种选择,表面着色器(Surface Shader)、顶点/片元着色器和固定函数着色器。无论使用哪一种形式,真正意义上的Shader代码都是包含在ShaderLab语义块中的,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Shader "MyShader" {
Properties {
// ...
}

SubShader {
// 真正意义上的Shader代码:
// 表面着色器(Surface Shader)或者
// 顶点/片元着色器(Vertex/Fragment Shader)或者
// 固定函数着色器(Fixed Function Shader)
}

SubShader {
// 与前一个SubShader类似
}
}

表面着色器

表面着色器是Unity自创的一种着色器代码类型,可以认为是Unity对顶点/片元着色器的更高一层的抽象,所需的代码量非常少,但是渲染的代价会比较大。使用表面着色器时,Unity会帮我们处理很多光照细节,以增加编写shader的便利性。

以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Shader "Custom/Simple Surface Shader" {
SubShader {
Tags { "RenderType" = "Opaque" }
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语义块中的CGPROGRAMENDCG之间。这是因为表面着色器不需要开发者关心使用多少个Pass、每个Pass如何渲染等问题,这些工作Unity会在背后替我们完成。

顶点/片元着色器

可以使用CG/HLSL在Unity中编写顶点/片元着色器,他们会更复杂但是灵活性也更高,以下是一段示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Shader "Custom/Simple VertexFragment Shader" {
SubShader {
Pass {
CGPROGRAM

#pragma vertext vert
#pragma fragment frag

float4 vert(float4 v : POSITION) : SV_POSITION {
return mul (UNITY_MATRIX_MVP, v);
}

fixed4 frag() : SV_Target {
return fixed4(1.0,0.0,0.0,1.0);
}
ENDCG
}
}
}

与表面着色器不同,顶点/片元着色器的代码是写在Pass语义块内,而非SubShader内的。这是因为我们需要自己定义每个Pass使用Shader的代码。虽然我们可能需要编写更多的代码,但是带来的好处是灵活性更高。更重要的是,我们可以控制渲染的实现细节。

固定函数着色器

之前的两种Unity Shader形式都使用了可编程管线。对于不支持可编程管线着色器的设备就需要使用固定函数着色器来完成渲染。这些着色器往往只可以完成一些非常简单的效果。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
Shader "Custom/Simple Fixed Function Shader" {
Properties {
_Color ("Main Color", Color) = (1,0.5,0.5,1)
}
SubShader {
Pass {
Material {
Diffuse [_Color]
}
Lighting On
}
}
}

固定函数着色器的代码被定义在Pass语义块中,这些代码相当于Pass中的一些渲染设置。对于固定函数着色器,我们需要完全使用ShaderLab的语法(即使用ShaderLab的渲染设置命令)来编写,而不是CG/HLSL。

选择Unity Shader的形式

  • 除非必须,尽量不要使用固定函数着色器
  • 如果需要和各种光源打交道,建议使用表面着色器,但是需要注意其在移动平台的性能表现
  • 如果涉及到的光照数目非常少,例如只有一个平行光,请选择顶点/片元着色器
  • 如果有很多的自定义渲染效果,请选择顶点/片元着色器

Unity Shader的示例

Unity3D文档中的一些shader的示例可用作参考学习:

REFERENCE

https://docs.unity3d.com/Manual/SL-Reference.html

《Unity Shader入门精要》