前面我们讲了UnityShader文件结构以及ShaderLab语法。接下来我们看看着色器代码应该写在哪里。

表面着色器可以写在SubShader语义块中;顶点/片元着色器和固定函数着色器可以写在Pass语义块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Shader "MyShader"{
Properties{
//所需的各种属性
}
SubShader{
//真正意义上的Shader代码会出现在这里
//Surface Shader
//Vertex Shader/Fragment Shader
//Fixed Function Shader
}
SubShader{
//和上一个SubShader类似
}
}

Surface Shader

Surface Shader是Unity自创的,需要的代码量少,渲染代价比较大,在背后依旧会转变成相应的Vertex/Fragment Shader。Unity包装好的Surface Shader处理了很多光照细节,来简化Vertex/Fragment 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语义块中的CGPROGRAMENDCG之间,不需要开发者使用Pass,只需要定义各种纹理和光照模型即可。

CGPROGRAMENDCG之间的代码是用Cg/HLSL编写的,我们需要把Cg/HLSL语言嵌套在ShaderLab语言中。这里的Cg/HLSL是Unity封装后提供的,它的语法和标准的Cg/HLSL几乎一样但还是有细微的不同,有些原生函数和用法Unity并没有提供支持。

Vertex/Fragment Shader

在Unity中我们可以使用Cg/HLSL语言来编写顶点/片元着色器,它们更加复杂,但灵活性也更高。

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 vertex 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
}
}
}

顶点/片元着色器也需要定义在CGPROGRAMENDCG之间,而且需要在Pass语句块内。我们需要定义每个Pass的代码,提高了灵活性,可以控制渲染的实现细节。

Fixed Function Shader

Fixed Function Shader也需要被定义在Pass语义块中,需要完全使用ShaderLab语法,而非使用Cg/HLSL。Fixed Function Shader是为了支持较古旧的设备(DirectX7.0、OpenGL1.5、OpenGL ES1.1),现在已经被淘汰。

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

一些建议

  • 如果想和各种光源打交道,可以使用表面着色器,但需要小心它在移动平台的表现。
  • 如果光照数目非常少,例如只有一个平行光,那么使用顶点/片元着色器。
  • 如果有很多自定义风格化渲染效果,请选择顶点/片元着色器。

一些注意的点

Unity Shader不是真正的Shader

在Unity里,Unity Shader实际上指的是一个ShaderLab文件,它以“.shader”作为后缀。

真正的Shader Unity Shader
仅可以编写特定类型的Shader,例如顶点着色器、片元着色器等。 可以在同一个文件里同时包含需要的顶点着色器和片元着色器代码
无法设置渲染。 可以通过特定的指令来设置Blend、ZTest等
需要编写冗长的代码来设置着色器的输入和输出,要小心处理输入输出的位置对应关系等。 只需要在Property语句块中声明属性,就可以靠材质来方便地改变。而对于模型自带地数据(顶点位置、纹理坐标、法线等),UnityShader提供了直接访问的方法。

UnityShader的缺点:

  • Unity仅在DirectX11平台下提供曲面细分着色器(Tessellation Shader)、几何着色器(Geometry Shader)的相关功能。

  • 一些高级的Shader语法Unity Shader也不支持

Unity Shader和Cg/HLSL之间的关系

Unity Shader是用ShaderLab语言来编写的,Cg/HLSL通过嵌套在ShaderLab的CGPROGRAMENDCG之间来发挥作用。Cg和DX9风格的HLSL几乎是同一种语言,因此在Unity里Cg和HLSL是等价的。

1
2
3
4
5
6
7
8
9
10
11
12
Pass{
//Pass的标签和状态设置

CGPROGRAM
//编译指令,例如
#pragma vertex vert
#pragma fragment frag

//Cg代码
ENDCG
//其他一些设置
}

表面着色器中,Cg/HLSL代码写在SubShader语句块中,我们可以通过一个表面着色器的Inspector窗口点击Show generated code来查看真正生成的顶点/片元着色器代码,可以发现Unity最终还是把它变成了多Pass的,Cg/HLSL语句依然在Pass语句块内。

从本质来讲,Unity中只存在顶点/片元着色器,表面着色器的固定函数着色器到最后都会转化成顶点/片元着色器。

Unity编辑器会把这些Cg片段编译成低级语言,如汇编语言等。通常Unity会使用不同的编译器把这些Cg片段编译到所有相关平台(Direct3D 9、OpenGL、Direct3D 11、OpenGL ES等)上,这样就不会在切换平台时再重新编译,而且如果代码在某些平台上发生错误就可以立即得到错误信息。

shader_compile_code

在Unity Shader的Inspector面板可以通过Compile and show code按钮来查看Unity对Cg片段编译后的代码。通过单击Compile and show code按钮右端的倒三角可以打开下拉菜单,在这个下拉菜单中可以选择编译的平台种类,如只为当前的显卡设备编译特定的汇编代码,或为所有的平台编译汇编代码,我们也可以自定义选择编译到哪些平台上。

当发布游戏的时候,游戏数据文件中只包含目标平台所需要的编译代码,不需要的是不会放进去的。

使用GLSL来写UnityShader

GLSL的代码需要嵌套在GLSLPROGRAMENDGLSL之间,GLSL的代码仅支持Mac OS X、OpenGL ES 2.0和Linux。

Unity - Manual: GLSL in Unity

拓展链接

Unity - Manual: ShaderLab

Writing custom shaders | Universal RP | 12.1.7 (unity.cn)

The Cg Tutorial - Chapter 1. Introduction (nvidia.com)