Unity Shader 入门(0)——ShaderLab
摘选并翻译自《Building Quality Shaders for Unity》。
Writing a Shader in Unity
为了处理图像,像Unity这种程序会使用一些流行的图形接口(graphics APIs).每个图形接口都有对应的着色语言(shading language):
- OpenGL接口,一个受欢迎的跨平台的图形库,使用叫做GLSL的着色语言。
- DirectX,被设计用于Microsoft平台,使用HLSL语言。
- Cg, 由NVIDIA开发但是已经废弃的着色语言。
渲染语言是你写shader代码的地方,游戏或者游戏引擎会编译这些shader,然后才能运行在GPU上。虽然理论上你可以使用GLSL,HLSL,或者Cg中的任意一个,但是现代Unity shaders是用HLSL写的。
Unity在这个基础上又设计了ShaderLab,用于包装刚刚提到的着色语言。所有基于代码的shader在Unity中都以ShaderLab的语法书写,这样能够一次性实现以下效果:
- ShaderLab使得UnityEditor,C#脚本,以及底层的着色语言(shader language)能够交互。
- ShaderLab能够更方便地修改shader的通用设置。在其他游戏引擎,你可能需要探索复杂的配置窗口或者需要用到图形API代码来修改blend, clipping或者culling相关的设置,但在Unity,我们可以直接在ShaderLab中通过命令(command)实现。
- ShaderLab提供了层叠式的语法,这使得我们能够在一个文件中写多个shader。Unity会找到并运行第一个兼容的shader。这意味着我们可以为不同的硬件或者渲染管道设计不同的shader。
实战能够帮助我们更容易地理解这些是怎么工作的,现在我们来写一些ShaderLab吧。
Writing ShaderLab Code
在这个例子中,我们会编写shader来显示一个纯色的物体。并且我们可以通过UnityEditor中的选项修改对应的颜色。
创建HelloWorld.shader文件并打开。首先,我们需要使用Shader关键字来命名当前的着色器(shader)。 在声明shader的名称后,shader剩余部分会被包裹在一对花括号内。
Shader "Examples/HelloWorld"
{
}
我们需要在括号内声明Material的属性。这些属性可以看做是shader的变量,并且当我们在UnityEditor点击Material的时候,这些变量会出现的Inspector里面。属性(Properties)非常有用,因为他们允许我们为同一个shader,创建多个不同变量的Material。
我们会看到多种类型的属性,但是现在,我们会添加一个Color类型的属性。声明一个属性的语法和声明Shader的语法很像。
Shader "Examples/HelloWorld"
{
Properties
{
_BaseColor(“Base Color”, Color) = (1,1,1,1)
}
}
声明属性的这行代码有多个组成部分,我们需要将这个奇怪的语法拆分理解:
- 按照规定,属性的名称需要以下划线
_为开头,然后我们需要大写每个单词的首字母。这个例子中,_BaseColor为 计算机可读(computer-readable)的名称,并且我们会在代码中引用_BaseColor,所以它又被称为引用(Reference)。 - 在括号内,我们首先需要用双引号指定用户可读(human-readable)的名称,这会在Inspector面板中显示。
- 然后是变量的类型,这里是
Color,我们也可以有Texture2D,Cubemap,Float这样的类型。 - 最后,我们在等号后面设置属性的默认值,在创建新的Material时,属性会设置成默认值。
在Shader里,我们使用0和1之间的浮点数来表示每个颜色通道(红绿蓝,不透明度)。(1,1,1,1)表示完全不透明的白色。
处理完属性部分后,我们需要添加一个Subshader. 一个SubShader是一套渲染方案。
Adding a SubShader
虽然我们可以添加多个Subshader,在这个例子中,我们只会用到一个。
Shader "Examples/HelloWorld"
{
Properties { ... }
SubShader
{
}
}
如果你定义了多个SubShader块,Unity会挑选第一个能够在目标硬件和渲染管道工作的Subshader。 如果没一个Subshader能够满足,那么shader就会编译失败。这时Unity会显示洋红色的材质。
Unity设计了回退(Fallback)系统,你可以指定一个候选的shader文件,如果每个Subshader都不兼容,那么Unity会使用那个候选shader。
如果你不希望使用回退系统,你可以不添加这个关键字,或者显示声明Fallback Off
Shader "Examples/HelloWorld"
{
Properties { ... }
SubShader { ... }
SubShader { ... }
Fallback "Unlit/Color"
}
在Subshader里面,我们可以添加多个设置来控制shader的运行,在这里,我们先只添加一个: Tags。
SubShader Tags
Tags代码块允许我们指定shader是透明的还是不透明的。当前对象是否渲染在其他对象之后,以及指定哪个渲染管道。每个Tag都是string的键值对,第一个string是tag的name,第二个string是它的值。 让我们添加RenderTypetag来设置当前对象为opaque rendering。
We can add code comments in shaderLab in a similar manner to C-style languages: single-line comments start with a double forward slash //, and multiline comments are enclosed between /* and */
SubShader
{
Tags
{
// Render alongside other opaque objects.
"RenderType" = "Opaque"
}
}
在Tags代码块中,我们还可以指定Queue来确定什么时候画这个对象。上文我给出了一个简化版的draw顺序:所有的opaque对象先draw,然后是所有透明的物体。但实际上更复杂一点。Queue是一个int类型的值, 数值低的先被渲染。它有一些预设值,分别是:
- Background = 1000
- Geometry = 2000
- AlphaTest = 2450
- Transparent = 3000
- Overlay = 4000
如果你想使用预设值以外的数值,你可以通过加或减实现。比如设置Queue的值为1500,我们可以使用Background+500或者是Geometry-500。对于不透明的物体,我们经常使用Geometry。
Tags
{
"RenderType" = "Opaque"
"Queue" = "Geometry"
}
Adding a Pass
Pass是实际上我们会添加shader代码的地方,一个pass意味着渲染一个对象的完整循环;
一个Subshader可以包含多个Pass代码块,如果有多个,Unity会从上到下运行每个Pass。
SubShader
{
Tags { ... }
Pass
{
}
}
Unity Shader使用 HLSL,我们需要将shader代码 放在 HLSLPROGRAM 和 ENDHLSL指令 之间。
SubShader
{
Tags { ... }
Pass
{
HLSLPROGRAM
// HLSL code goes in here.
ENDHLSL
}
}
终于,我们要开始写一些HLSL代码了。多么令人兴奋!从这里开始,所有的代码都会被放在 HLSLPROGRAM 和 ENDHLSL指令 之间。
接下来我们写的就不是ShaderLab语言,而是HLSL语言。 我们需要为我们的shader做一些准备。
Pragma Directives and Includes
接下来,我们需要编写vertex和fragment两个函数来确定shader的作用。我们需要告诉Unity哪个函数是vertex shader,哪个函数是fragment shader。我们可以通过特殊的预处理指令来完成。
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDHLSL
在写shader的时候,我们会经常遇到#pragma语句。我们使用他们来定义shader函数,就像上面的代码, 也可以用来指定shader编译到特定的平台,或者是开启某个硬件的效果。
除此之外,我们还会用到#include指令,来包含其他的shader文件。Unity提供了大量的shader include 文件。对于内置渲染管线,我们可以在 [Unity root installation folder]/Editor/Data/CGIncludes目录发现这些文件。其中最重要以及频繁被使用的文件是UnityCG.cginc。虽然它是cginc后缀,但它是和HLSL兼容的。
在每个shader中,我们会include一个标准库文件,对于内置渲染管线,我们必须include UnityCG.cginc文件。
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
Controlling Data Flow with Structs
渲染管线的第一步就涉及到收集场景中的数据然后传递给shader,在shader这边,我们需要设计一些方法来获得数据。
在shader不同的阶段,我们通过叫做结构体structs的容器来传递数据,这个结构体会包含一组变量。 我们下面讲的第一个结构体包含了 我们想从mesh模型面片获取的数据,并会传递给vetex shader。
The appdata Struct
我们通常命名这个结构体为 appdata, VertexInput, 或者Attributes;虽然你可以选择任意的名称,但我偏向于appdata因为Unity内置的结构体就是像这样命名的。
每个appdata 实例(instance)包含Mesh上一个顶点(vertex)的数据,现在我们只需要其中的position。 Vertex的position是定义在对象空间(object space),每个位置是相对于mesh的原点。
HLSL要求我们给每个变量添加语义semantic。不要慌张,这个语义很简单,它就是标记了在 下一个shader阶段这个变量的作用。 比如 vertext positions需要标记 POSITION语义。
a full list of semantics can be found on the Microsoft hLsL website. at the time of writing, it can be found here: https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics
#include "include-file-for-your-pipeline"
struct appdata
{
float4 positionOS : POSITION;
};
The v2f Struct
This struct is commonly called v2f, VertexOutput, or Varyings, but I will be stickingwith v2f, which stands for “vertex-to-fragment.”
接下来我们要讲的结构体叫做 v2f, 意思是 vertex-to-fragment,表示这个结构体是从 vertex着色器 到 fragment着色器的结构体。
回忆一下,光栅化的步骤发生在顶点着色器vertex shader之后, 像素着色器fragment shader之前。我们需要知道vertex shader会输出什么。对于我们第一个shader,我们只需要输出每个顶点裁剪坐标系的位置,这个变量名因此叫做positionCS。 (第二章有讲clip space)。
struct appdata { ... };
struct v2f
{
float4 positionCS : SV_POSITION;
};
你可能注意到了语义这里有些不同,HLSL对于输入和输出的Position会区分对待,所以我们这里使用SV_Position语义。
System-value semantics are new to Direct3D 10. All system-values begin with an SV_ prefix, a common example is SV_POSITION, which is interpreted by the rasterizer stage.
Variables in HLSL
虽然我们在Properties代码块里面声明了_BaseColor属性。我们在HLSL里面需要再定义一下。(一个是在ShaderLab,一个是在HLSL,他们负责的功能是不同的,ShaderLab的部分用于对接UnityEditor,而HLSL则会被编译成GPU代码。)
即使变量没有在Properties里面被声明,我们还是可以在HLSL里面声明的。那样的话,我们需要用到C#脚本来设置这些变量的值,而不是在Inspsector面板修改这些变量。
_BaseColor很明显,表示一个颜色,在HLSL没有特殊对应的类型,我们可以用float4表示。
unity may also generate certain shader variables for us. We need to declare some of them inside hLsL, but we won’t need to include them in Properties or pass the data to the shader ourselves with scripting. an example of this kind of variable is _CameraDepthTexture, which we will see later.
struct v2f { ... };
float4 _BaseColor;
The Vertex Shader
vertex shader 函数需要将vertex position从对象空间(object space)转化到裁剪空间(clip space),这通常会涉及到一系列的变换: from object to world space, then from world to view space, and then from view to clip space。 第二章里有详细描述这个过程。
它们的组合变换(combined transformation)被称为 model-view-projection 变换,Unity为我们提供了一个函数来应用这个变换。
在内置管道,这个函数被称为 UnityObjectToClipPos,这个函数会将 object-space的位置转化为 clip-space的位置。
vertex函数,也就是我们的vertex shader, 和其他函数一样,有一个返回类型,以及一系列的参数。
v2f vert (appdata v)
{
v2f o;
o.positionCS = UnityObjectToClipPos(v.positionOS);
return o;
}
The Fragment Shader
像素着色器函数frag的唯一参数是 v2f结构体,以及返回类型是float4,表示每个像素的颜色。 两个函数的关键不同是我们需要为像素着色器的输出设置语义,SV_TARGET
float4 frag (v2f i) : SV_TARGET
{
return _BaseColor;
}
虽然我们没有用到 v2f里面的字段,Unity会自动使用SV_POSITION语义的变量来光栅化对象,使其变为像素。因此设置v2f结构体是有意义的。
We have successfully written a shader that renders an object in a single color with no
lighting, which is about as “Hello World” as you can get. Congratulations for making it to
this stage!

浙公网安备 33010602011771号