Unity Shader学习笔记(一) - ShaderLab概述

本文最后更新于 2025年1月11日 下午

返璞归真。

Unity Shader入门精要

本笔记无基础部分,直接从实践开始。

材质与Shader概述

材质

材质作用于GO(准确来说,作用于MeshRendererParticle System等组件),依赖于Shader,被Shader定义。

通过材质Inspector的Shader下拉栏,可切换此材质依赖的Shader。

右下角为材质展示框,展示框右上角的按钮从左到右依次为:播放动画(不常用)、切换材质预览模型(除球体外还有立方体、圆柱体)、切换光照类型(双光源、单光源)、切换反射探针(用于金属度、光滑度高的镜面材质预览)。

image-20250111103452781

Shader

Unity包含下列内置Shader类型:

  • Standard Surface Shader:标准表面着色器,为经过改良的PBR着色器。
  • Unlit Shader:不包含光照,但包含雾效(在全局雾中使用此Shader的物体是否在雾中进行混合)的着色器。
  • Image Effect Shader:屏幕后处理着色器。
  • Compute Shader:计算着色器。
  • Raytracing Shader:实验性,光追着色器,用于替代原本在Compute Shader中实现的光线投射算法。仅在HDRP中生效。

Unity Shader本质上是文本文件,具有Import Settings面板。

image-20250111111442051

在Default Map设置中,我们可以设置使用此Shader的材质的某些纹理属性所使用的默认纹理。在下半部分的面板上,我们可以看到与此Shader有关的信息。

对于表面着色器,可以单击Show generated code查看由表面着色器生成的VS和FS。单机Compile and show code可以查看此着色器针对不同图形API编译成的Shader代码,用于更底层地分析、优化Shader。

ShaderLab

ShaderLab为Unity中编写Unity Shader的说明性语言。其使用**语义(Syntax)**描述文件结构。ShaderLab定义了材质所需的所有属性,而非仅仅着色器代码。

其基础结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
Shader "ShaderName"{
Properties{
// 材质属性
}
SubShader {
// 显卡A使用的着色器
}
SubShader {
// 显卡B使用的着色器
}
Fallback "VertexLit"
}

下面逐项解释。

结构

ShaderName

Unity Shader文件的第一行通过Shader语义指定该着色器的名字。使用斜杠可以控制此Shader位于哪个子级菜单中。完成名称定义的Unity Shader,可以在材质检视器的Shader下拉栏找到此Shader。

image-20250111112225137

Properties

属性在Properties语义块中定义,此处定义的属性将会出现在材质面板。

1
2
3
4
Properties{
Name ("display name", PropertyType) = DefaultValue
Name ("display name", PropertyType) = DefaultValue
}

注意,属性之间只需要换行,无需分号。

在上面的示例代码中:

  • Name为属性本身在Shader中的变量名,通常使用一个下划线开头
  • “display name”为属性在材质面板中现实的名字,通常使用大写字母开头;
  • PropertyType为属性的类型,示例如下:
类型 定义语法 例子
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.0
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”{}

我们可以通过重载默认材质编辑面板的形式让材质检视器支持布尔值等更多类型。

Unity引擎自定义ShaderGUI_unity_阿赵3D-Unity官方开发者社区

Property语义块的作用仅仅是让Shader内部属性得以在检视器中显示。

SubShader

每个Unity Shader可以包含至少一个SubShader语义块。Unity Shader被加载后,Unity会选择第一个能够在目标平台上运行的SubShader。如果一个可运行的SubShader都没有,就会使用Fallback语义指定的Unity Shader。

其结构如下:

1
2
3
4
5
6
7
8
9
10
SubShader{
//可选
[Tags]
//可选
[RenderSetup]
Pass{
//渲染步骤
}
//可添加多个Pass
}

RenderSetup定义了渲染状态。如下:

状态 设置指令 解释
Cull Cull Back/Front/Off 剔除背面/正面/关闭剔除
ZTest ZTest Less Greater/LEqual/GEqual/Equal/NotEqual/Always 深度测试设置
ZWrite ZWrite On/Off 开启/关闭深度写入
Blend Blend SrcFactor DstFactor 开启、设置混合模式

Tag定义了渲染标签。其声明结构为Tags { “TagName1” = “Value1” “TagName2” = “Value2” }。如下:

标签 说明 例子
Queue 指定渲染顺序。可以使用内置的渲染顺序组,也可以自定义渲染顺序(数字)。 Tags { “Queue” = “Transparent” }
RenderType 指定着色器类型,例如透明/不透明。为着色器替换功能提供便利。 Tags { “RenderType” = “Opaque” }
DisableBatching 是否使用批处理。在某些特殊情况下,使用批处理时会导致渲染错误(如使用模型空间坐标进行顶点动画)。 Tags { “DisableBatching” = “True”}
ForceNoShadowCasting 是否投射阴影。 Tags { “ForceNoShadowCasting” = “True”}
IgnoreProjector 是否忽略Projector组件的影响。Projector组件用于构造贴画、弹孔、动态阴影等。 Tags { “IgnoreProjector” = “True” }
CanUseSpriteAtlas 当此SubShader作用于Sprite时,设为False Tags { “CanUseSpriteAtlas” = “False” }
PreviewType 指明默认情况下如何预览材质 Tags { “PreviewType” = “Plane”}
Pass
1
2
3
4
5
6
Pass {
Name "PassName"
[Tags]
[RenderSetup]
//其他代码
}

Pass可用的标签有:

标签 说明 例子
LightMode 定义该Pass在渲染管线中的角色。包括Always(始终渲染,不应用照明)、ForwardBase(前向渲染,应用环境光、平行光、顶点光照、光照贴图)、ForwardAdd(前向渲染,应用逐像素光照)、Deferred(延迟渲染)、ShadowCaster(渲染到深度贴图/纹理)等。 Tags { “LightMode” = “ForwardBase”}
RequireOptions 指定满足某些条件时才渲染此Pass。仅支持SoftVegetation Tags { “RequireOptions” = “SoftVegetation” }

此外,UsePass命令可以直接复用其他Unity Shader中的Pass,无需再次定义;GrabPass命令可以抓取屏幕并将结果存储在纹理中供Pass使用。

Fallback

如果所有的SubShader均无法在此设备上运行,则使用Fallback语义块定义的Unity Shader。

Fallback “name”

实际上,Fallback语义块会影响阴影投射。在投射阴影时,Unity会查找用于阴影投射的Pass。一般我们不需要自己写一个Pass,因为Fallback使用的内置Shader包含了这么一个Pass。所以Fallback不能乱设置。

形式

Unity Shader的核心是着色器代码。着色器代码可以写在SubShader中(表面着色器),也可以写在Pass中(顶点/片段着色器)。

表面着色器

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

上面展示了最基本的表面着色器。着色器代码被定义在CGPROGRAMENDCG关键字之间。

表面着色器不关心要使用多少Pass,每个Pass如何渲染,只关心如何填充颜色、法线,使用何种光照模型。剩余的部分都由Unity为我们解决。

顶点/片段着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}
float4 frag(): SV_TARGET{
return fixed4(1.0,0.0,0.0,1.0);
}
ENDCG
}
}
}

上面展示了最基本的顶点/片段着色器。

Unity Shader内置变量

数学相关

位于UnityShaderVariables.cginc中。

变换矩阵

变量名 说明
UNITY_MATRIX_MVP MVP矩阵,将局部坐标变换到裁剪坐标,供片段着色器使用
UNITY_MATRIX_MV
UNITY_MATRIX_V
UNITY_MATRIX_P
UNITY_MATRIX_VP
UNITY_MATRIX_T_MV UNITY_MATRIX_MV的转置。通过截取该矩阵的前三行和前三列(摒弃平移影响),可以将方向向量从观察空间变换到模型空间。
UNITY_MATRIX_IT_MV UNITY_MATRIX_MV的逆转置矩阵,用于将法线从模型空间变换到观察空间。转置可得到UNITY_MATRIX_MV的逆矩阵
_Object2World 当前的M矩阵,将局部坐标变换到世界坐标
_World2Object _Object2World的逆矩阵

将观察空间的顶点/矢量变换到模型空间的操作如下:

1
float4 modelPos = mul(transpose(UNITY_MATRIX_IT_MV), viewPos);

相机/屏幕参数

变量 类型 说明
_WorldSpaceCameraPos float3 相机的世界空间坐标
_ProjectionParams float x = 1.0, y = Near, z = Far, w = 1.0 + 1.0/Far
_ScreenParams float4 x = width, y = height, z = 1.0+1.0/width, w = 1.0+1.0/height。其中width和height为相机RT对应的像素宽和像素高
_ZBufferParams float4 x = 1-Far/Near, y = Far/Near, z = x/Far, w = y/Far。用于线性化Z-Buffer中的深度值
unity_OrthoParams float4 x = width, y = height, z无定义, w = 1.0或0.0(正交1.0, 透视0.0)。
unity_CameraProjection float4x4 相机投影矩阵
unity_CameraInvProjection float4x4 相机投影矩阵逆矩阵
unity_CameraWorldClipPlanes float4 相机的6个裁剪平面在世界空间下的等式。按左、右、下、上、近、远裁剪平面排列。

备注

  • mul默认使用右乘的方式,按列矩阵进行乘法。即,要将向量v通过矩阵M进行变换,代码为:

float4 result = mul(M,v)

Unity内置矩阵变量按列存储。

  • 使用一串float填充矩阵时,使用的是行优先填充。

注意,C#中的Matrix4x4为列优先填充。

使用VPOSWPOS语义可以获取片段对应的屏幕坐标。将其xy分量除以屏幕分辨率,可以得到视口空间坐标。

也可以通过ComputeScreenPos函数,从裁剪坐标(经过UNITY_MATRIX_MVP矩阵变换的vert pos)计算得到未经齐次除法的视口坐标。后续在片段着色器中除以w分量得到真正的视口坐标。这是因为在顶点着色器中直接进行齐次除法时(变换到投影空间),由于投影空间的非线性特性,会导致插值不准确。

Unity Shader

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
Shader "Custom/Learn Shader"{
Properties{
_Color("Color", Color) = (1.0,1.0,1.0,1.0)
}
SubShader{
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 _Color;
struct appdata{
float4 vertex : POSITION;
float4 normal : NORMAL;
float4 uv : TEXCOORD0;
}
struct v2f{
float4 vertex : SV_POSITON;
fixed3 color : COLOR0; // 顶点颜色语义
};
v2f vert(appdata i){
v2f o;
o.vertex = UnityObjectToClipPos(i.vertex);
o.color = v.normal * 0.5 + fixed3(0.5,0.5,0.5);
return o;
}
fixed4 frag(v2f i) : SV_TARGET{
fixed3 c = i.color;
c *= _Color.rgb;
return fixed4(c,1.0);
}
ENDCG
}
}
}

类型对应

要想让Properties语义块中的属性能在CG/HLSL中使用,就需要在CG/HLSL块中定义对应类型,同变量名的变量。ShaderLab类型和CG/HLSL变量类型对应如下:

ShaderLab类型 CG变量 HLSL变量
Color, Vector float4, half4, fixed4 float4
Range, Float float, half, fixed float
2D sampler2D Texture2D
Cube samplerCube TextureCube
3D sampler3D Texture3D

内置文件与变量

在CG块的开头,可以使用#include.cginc,即CG头文件引入。

常用的UnityCG库有:

  • UnityCG.cginc,包含了最常使用的辅助函数、宏和结构体。
  • UnityShaderVariables.cginc,包含了许多内置全局变量,如UNITY_MATRIX。编译时自动引入。
  • Lighting.cginc,包含了各种内置光照模型
  • HLSLSupport.cginc,包含了跨平台编译的宏和定义。编译时自动引入。
  • UnityStandardBRDF.cginc,用于PBR渲染。
  • UnityStandardCore.cginc,用于PBR渲染。

UnityCG.cginc有以下常用结构体与函数:

结构体名称 描述 包含变量
appdata_base VS输入 位置、法线、一组纹理坐标
appdata_tan VS输入 位置、切线、法线、一组纹理坐标
appdata_full VS输入 位置、切线、法线、多组纹理坐标
appdata_img VS输入 位置、一组纹理坐标
v2f_img VS输出 ClipPos位置、纹理坐标
帮助函数名称 描述
float3 WorldSpaceViewDir(float4 v) 输入模型空间顶点位置,返回世界空间中从该点到相机的View Dir。
float3 ObjSpaceViewDir(float4 v) 输入模型空间顶点位置,返回模型空间该点到相机的View Dir。
float3 WorldSpaceLightDir(float4 v) 用于Forward渲染。输入模型空间顶点位置,返回世界空间中该点到光源的Light Dir,未归一化。
float3 ObjSpaceLightDir(float4 v)
float3 UnityObjectToWorldNormal(float3 norm) 将模型空间法线转换到世界空间
float3 UnityObjectToWorldDir(float3 dir) 方向矢量从模型空间到世界空间
float3 UnityWorldToObjectDir(float3 dir)

语义

语义(Semantics)可以让Shader知道从哪里读取数据,并把数据输出到哪里。

SV_开头的语义是系统数值(System Value)语义。使用该语义描述的变量不可随意赋值,因为流水线需要使用它们完成特定的目的。例如,SV_POSITION必须存储裁剪空间的顶点坐标。

Unity支持以下语义:

语义 描述
POSITION 模型空间顶点位置输入,float4类型
NORMAL 顶点法线,float3类型
TANGENT 顶点切线,float4类型
TEXCOORDn 顶点纹理坐标,float2或float4类型
COLOR 顶点颜色,fixed4或float4类型

从VS传递数据给FS所用的结构体中,一般包含SV_POSITIONCOLOR0(第一组顶点颜色,可选)、COLOR1(第二组顶点颜色,可选)、TEXCOORD0~TEXCOORD7(纹理坐标,可选)。

尽管我们定义这些变量用于传输颜色、纹理坐标等,但实际上我们可以把任何数据都传入其中(不包括SV语义)。

SV_Target语义用于标记FS输出。


Unity Shader学习笔记(一) - ShaderLab概述
http://example.com/2025/01/11/Unity-Shader学习笔记(一)-ShaderLab概述/
作者
Yoi
发布于
2025年1月11日
许可协议