2024-12-29
Shader 101-02 - ShaderLab Intro ShaderLab 是什么 ShaderLab 是 Unity3D 提供的 Shader 程序,在 Unity 中所有的 Shader 程序都是使用名为「ShaderLab」的声明性语言编写。与 HLSL 与 GLSL 相比,ShaderLab 是一层更高级的封装,通过使用 ShaderLab 我们可以更方便的编写着色器代码。
当前 Shader 中主要逻辑是 HLSL写的,ShaderLab 只是 Unity3D 对着色器语言进行的一层封装(如 HLSL 和 GLSL等),我们可以将 ShaderLab 理解为一种「注释型」语言。
ShaderLab 是 Unity3D 构建的一种方便开发者做跨平台 Shading 开发的语言体系,它主要包括 ShaderLab Text
, ShaderLab Compiler
, ShaderLab Asset
和 ShaderLab Runtime
四个部分。
ShaderLab Text 也就是 ShaderLab 的文本,指的其实就是我们在 .shader
文件中写的那些代码。
ShaderLab Compiler 用于编译 ShaderLab Text,因为 ShaderLab Text 虽然通常会使用HLSL编写,然而实际上HLSL并不能直接运行在 DirectX 设备上。如同一堆 cpp 文件如 果没有被编译成本机程序就无法被计算机执行,它需要一个翻译转换成计算机可以使用的机器语言的过程。具体信息可以查看相关文档。
ShaderLab Text 通过 ShaderLab Compiler 翻译过后,得到的东西就叫做 ShaderLab Asset。
由于 ShaderLab Text 是一种有语法结构的语言,编译成 ShaderLab Assets 后也会包含很多的信息,因此 Shader Runtime 被构建出来,并在程序运行时利用这些信息。
我们目前并不需要知道太多底层的细节,只要大致知道 ShaderLab 包含了这四部分即可。
ShaderLab Text 的结构 ShaderLab Text 的结构初学的时候会让人非常疑惑,不知道为什么会这样写。不过计算机好就好在它是一个人造的学科,因此几乎任何的设计都是有迹可循的。接下来,我们将从功能的角度去讲解 ShaderLab Text 是如何设计的,又分为了哪些部分。
对于 ShaderLab ,我们可以简单划分为三个部分,Properties, SubShader 和 Pass。
Properties 用来让我们从外部为 Shader 提供数据,如果没有 Properties 我们便只能将相同的代码写无数遍,造成性能和存储的浪费。
SubShader 需要存在多个的原因是因为目前市面上的 GPU 形态各异,虽然功能大多相同,但每个 GPU 都有些自己特化的功能,这就会导致着色器在某些 GPU 上能够正常渲染,在其他的 GPU 上可能会有问题。
为了避免这种情况,在写着色器的时候通常会根据效果的高中低,分出多个不同版本的 SubShader,让各个 GPU 都能使用自己所能使用的最适合的着色器。如果所有的 SubShader 都无法正常使用,Fallback 就是最后的保底方案。
Pass 是通道的意思。官方定义中,通道控制 GameObject 几何体的一次渲染,写 Shader 一般都是在 Pass 代码块中控制渲染,我们的渲染代码大多也是写在 Pass 里。
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 "着色器的名称空间/ 着色器名字"
{
// 第二部分:材质面板上可以看到的属性
Properties
{
}
// 第三部分:顶点-片段着色器 或 表面着色器 或 固定函数着色器
SubShader
{
// 定义一个渲染通道(Render Pass)
// 一个 SubShader 中可以有多个 Pass
// 一个 Pass 是指 GPU 执行一次渲染操作所需的全部指令,它包含了一个绘制对象的具体方式和参数。
// GPU 按照 Pass 的定义顺序依次处理这些渲染通道,从第一个 Pass 开始,直到最后一个 Pass
Pass
{
}
Pass
{
}
}
// 第三部分:更精简的表面着色器(用于适配旧设备)
// 系统会从前往后依次尝试执行 SubShader
// 如果前一个无法被执行,那么就会尝试执行之后的 SubShader,直到找到一个能够运行的版本
SubShader
{
}
// 第四部分
Fallback "备用的Shader"
}
在一个 Pass 代码块内,代码的格式通常如下所示。
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
42
43
Pass
{
Tags { "LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal: NORMAL;
};
struct v2f
{
float4 pos: SV_POSITION;
float3 color : Color;
};
float4 _Diffuse;
v2f vert (appdata v)
{
v2f o;
// ....
return o;
}
float4 frag (v2f i) : SV_Target
{
// ....
return fixed4(i.color,1.0 );
}
ENDCG
}
第一次学习 Shader 的人在面对这段代码的时候通常会非常疑惑,通常疑问有如下几种
CGPROGRAM 和 ENDCG 是什么 pragma 后面的声明是什么意思 appdata 和 v2f 是什么 vert 和 frag 是什么,方法的格式代表着什么含义 CGPROGRAM & ENDCG CGPROGRAM & ENDCG 之间的代码是使用 CG/HLSL 语言编写的,也就是说,我们需要把 CG/HLSL 语言嵌套在 ShaderLab 语言中。
——《Unity Shader 入门精要》
我们可以将 CGPROGRAM 和 ENDCG 理解为一组配合使用的标识符,告诉 Unity 在这个范围里面我需要使用 CG/HLSL 语言来进行着色器代码的编写。
正如 CG/HLSL 关键字对应的是 CGPROGRAM & ENDCG 标识符一样,如果想使用 GLSL 语言进行着色器编写,可以使用 GLSLPROGRAM & ENDGLSL 将相关的代码包围起来。但是如果使用 GLSL 语言,就等于是放弃了 Windows 以及所有使用 DirectX 的平台,因此一般而言,我们只需要使用 CGPROGRAM & ENDCG 就够了。
CGPROGRAM & ENDCG 可以编写 表面着色器 和 顶点/片元着色器,区别在于表面着色器是被定义在 SubShader 语句块中,而顶点片源着色器被定义在 Pass 语句块中。
pragma & vert & frag pragma 是 Shader 的预编译指令,在以上的代码中,意思是为顶点和片源着色阶段指定特定的方法
1
2
3
4
5
// 指定顶点着色器所调用的方法
#pragma vertex vert
// 指定片源着色器调用的方法
#pragma fragment frag
appdata & v2f & vert & frag 只需要简单的观察我们就能发现,appdata 和 v2f 结构体是分别被 vert 和 frag 方法作为参数传递进函数的,v2f 结构体不仅是 vert 方法的输出,也是 frag 方法的输入。从直觉上我们就能隐约感觉到他们之间存在着某种联系,事实上也确实如此。
着色器中数据的传递流程如下图所示。
flowchart LR
GPU -- appdata --> vert --v2f--> frag
原理其实很简单,我们在 #pragma
中为顶点着色器和片源着色器分别指定了 vert 方法和 frag 方法,因此 Shader 执行到相应的流程时,就需要去调用 vert 和 frag,但是方法的执行需要参数,为了更方便的传递数据,Shader 提供了 POSITION
、TEXCOORD0
等关键字,这些关键字在 Shader 中称为语义,让 Shader 知道从哪里获取数据,并且还知道把输出的数据放在哪里。
SV_POSITION:描述的变量存储物体顶点在屏幕坐标上的位置,或者描述裁剪空间中的顶点坐标 COLOR:顶点颜色 Shader 中语义有很多,在这里就不一一列举了,如果之后有机会再专门说明。
事实上我们可以完全不使用结构体,直接在函数声明的时候使用这些语义去声明参数,但考虑到着色器程序每一个环节都需要非常非常多的参数,如果直接使用方法参数进行传递,那么方法的声明会变得异常的长,而且不好更改,因此使用结构体对所需的参数进行封装。
appdata 结构体是 vert 方法所需要的参数。
v2f 结构体是 vert 方法所输出的数据,同时也是 frag 方法所需要的参数,因此叫 v2f。名称中的 v 是 vertex shader,f 是 fragment shader,结合在一起就是 vertex shader to fragment shader,代表它是顶点着色器数据给片元着色器的数据。
深入 Shader 简单材质 在简单材质这个章节,我们将学习到 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Shader "Unlit/ SimpleImage"
{
Properties
{
_MainTex ("Texture", 2 D) = "white" {}
}
SubShader
{
Tags { "RenderType"= "Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
// sampler2D _MainTex: 用于采样纹理的句柄
// _MainTex_ST: Unity 自动生成的 UV 变换参数(偏移与缩放)
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
// 使用 UnityObjectToClipPos 将模型空间的 vertex 坐标转换为屏幕空间坐标 o.vertex
// 使用 TRANSFORM_TEX 对 uv 坐标应用 _MainTex_ST 的变换(缩放和偏移)
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// tex2D 是 Unity Shader 中用于纹理采样的一个核心函数
// 用于在片段着色器中从 2D 纹理中获取颜色数据的工具
// 使用 tex2D 从 _MainTex 纹理中采样颜色,采样点是传递的 uv 坐标
// 返回采样的颜色值作为最终像素颜色
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
修改UV的偏移 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
42
43
44
45
46
47
48
49
Shader "Unlit/ UVOffset"
{
Properties
{
_MainTex ("Main Texture", 2 D) = "white" {}
_UVOffset ("UV Offset", Vector) = (0 , 0 , 0 , 0 )
}
SubShader
{
Tags { "RenderType"= "Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
};
sampler2D _MainTex;
float4 _UVOffset;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv + _UVOffset.xy;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return tex2D(_MainTex, i.uv);
}
ENDCG
}
}
}
更改图像颜色 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
42
43
44
45
46
47
48
49
50
51
52
53
54
Shader "Custom/ ColorOverlay"
{
Properties
{
_MainTex ("Texture", 2 D) = "white" {}
_OverlayColor ("Overlay Color", Color) = (1 , 0 , 0 , 1 )
}
SubShader
{
Tags { "RenderType"= "Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _OverlayColor;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 texColor = tex2D(_MainTex, i.uv);
fixed4 result = texColor * (1 - _OverlayColor.a) + _OverlayColor * _OverlayColor.a;
return result;
}
ENDCG
}
}
}
四角色渐变 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
42
43
44
45
46
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD1;
float3 color : COLOR;
};
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
if (v.uv.x == 0 && v.uv.y == 0 ) {
o.color = float3(0 , 0 , 1 ); // Blue
} else if (v.uv.x == 0 && v.uv.y == 1 ) {
o.color = float3(1 , 0 , 0 ); // Red
} else if (v.uv.x == 1 && v.uv.y == 0 ) {
o.color = float3(0 , 1 , 0 ); // Green
} else if (v.uv.x == 1 && v.uv.y == 1 ) {
o.color = float3(1 , 1 , 0 ); // Yellow
}
return o;
}
float4 frag(v2f i) : SV_Target
{
return float4(i.color, 1 );
}
ENDCG
}
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
Shader "Unlit/ Rect2Circle"
{
Properties
{
_MainTex ("Texture", 2 D) = "white" {}
}
SubShader
{
Tags { "RenderType"= "Transparent" "Queue"= "Transparent" }
LOD 100
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// Sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// Calculate distance from the center of the texture
float2 center = float2(0.5 , 0.5 );
float dist = distance(i.uv, center);
// Create a circular mask
float radius = 0.5 ; // Adjust for desired circle size
if (dist > radius)
{
discard ; // Discard pixels outside the circle
}
return col;
}
ENDCG
}
}
}
控制图像圆角比例 图形的圆角控制的步骤有四步:
将片元的 UV 坐标转换到 $[-1, 1]$ 的区间 为 UV 坐标适配长宽比的缩放 判断当前片元是否远离圆心边缘 将边缘之外的片元的 alpha 设置为 0 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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
Shader "Custom/ RoundedQuad"
{
Properties
{
_MainTex ("Texture", 2 D) = "white" {}
_CornerRadius ("Corner Radius", Range(0 , 1 )) = 0.1
_Color ("Color", Color) = (1 , 1 , 1 , 1 )
}
SubShader
{
Tags { "Queue"= "Transparent" "RenderType"= "Transparent" }
Blend SrcAlpha OneMinusSrcAlpha
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata_t
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float2 worldPos : TEXCOORD1;
};
sampler2D _MainTex;
float _CornerRadius;
float4 _Color;
v2f vert (appdata_t v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.worldPos = v.vertex.xy;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// UV 坐标范围默认在 [0, 1] 之间
float2 uv = i.uv;
// 宽高比动态调整
float aspectRatio = abs(_ScreenParams.x / _ScreenParams.y);
// 计算相对于中心点的归一化 UV 坐标
float2 curPix = uv * 2.0 - 1.0 ; // [-1, 1]
curPix.x *= aspectRatio;
// 距离计算,用于判断片元是否在圆角区域内
// 表示当前片元相对于圆角边界的差值
// 如果 cornerCheck 是负值,说明该片元不位于圆角区域,此时不需要考虑圆角的效果。
// 如果 cornerCheck 是正值,说明该片元已经进入圆角区域,应该计算圆角效果。
float2 cornerCheck = abs(curPix) - (1.0 - _CornerRadius);
// 通过将每个分量与 0.0 比较,我们确保只有 大于或等于零的值 被保留下来,负值被截断为零。
// 这样做的目的是:
// 对于矩形区域内的片元(cornerCheck 为负)的距离会被置为零,表示它们不需要参与圆角计算。
// 对于进入圆角区域的片元,cornerCheck 的正值将保留,用于后续计算距离。
float dist = length(max(cornerCheck, 0.0 )) - _CornerRadius;
// 圆角过渡
// 当 dist <= 0.0,返回 0(完全不透明)
// 当 dist >= 0.01,返回 1(完全透明)
// 在 dist 从 0.0 到 0.01 的范围内,返回一个从 0 到 1 的平滑过渡值
float alpha = 1.0 - smoothstep(0.0 , 0.01 , dist);
// 纹理采样和颜色应用
fixed4 texColor = tex2D(_MainTex, uv) * _Color;
texColor.a *= alpha;
return texColor;
}
ENDCG
}
}
}