本课程全是用Built-in渲染管线

最基本的Shader代码

默认的Unlit 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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
Shader "CS02/MiniShader" //Shader的真正名字  可以是路径式的格式
{
/*材质球参数及UI面板
https://docs.unity3d.com/cn/current/Manual/SL-Properties.html
https://docs.unity3d.com/cn/current/ScriptReference/MaterialPropertyDrawer.html
https://zhuanlan.zhihu.com/p/93194054
*/
Properties
{
_MainTex ("Texture", 2D) = "" {} //不需要分号
_Float("Float", Float) = 0.0
_Slider("Slider", Range(0.0,1.0)) = 0.07
_Vector("Vector", Vector) = (.34, .85, .92, 1)
}
/*
这是为了让你可以在一个Shader文件中写多种版本的Shader,但只有一个会被使用。
提供多个版本的SubShader,Unity可以根据对应平台选择最合适的Shader
或者配合LOD机制一起使用。
一般写一个即可
*/
SubShader
{
/*
标签属性,有两种:一种是SubShader层级,一种在Pass层级
https://docs.unity3d.com/cn/current/Manual/SL-SubShaderTags.html
https://docs.unity3d.com/cn/current/Manual/SL-PassTags.html
*/
Tags { "RenderType"="Opaque" }
/*
Pass里面的内容Shader代码真正起作用的地方,
一个Pass对应一个真正意义上运行在GPU上的完整着色器(Vertex-Fragment Shader)
一个SubShader里面可以包含多个Pass,每个Pass会被按顺序执行
*/
Pass
{
CGPROGRAM // Shader代码从这里开始
#pragma vertex vert //指定一个名为"vert"的函数为顶点Shader
#pragma fragment frag //指定一个名为"frag"函数为片元Shader
#include "UnityCG.cginc" //引用Unity内置的文件,很方便,有很多现成的函数提供使用

//https://docs.unity3d.com/Manual/SL-VertexProgramInputs.html
struct appdata //CPU向顶点Shader提供的模型数据
{
//冒号后面的是特定语义词,告诉CPU需要哪些类似的数据
float4 vertex : POSITION; //模型空间顶点坐标
half2 texcoord0 : TEXCOORD0; //第一套UV
half2 texcoord1 : TEXCOORD1; //第二套UV
half2 texcoord2 : TEXCOORD2; //第二套UV
half2 texcoord4 : TEXCOORD3; //模型最多只能有4套UV

half4 color : COLOR; //顶点颜色
half3 normal : NORMAL; //顶点法线
half4 tangent : TANGENT; //顶点切线(模型导入Unity后自动计算得到)
}; //需要分号

struct v2f //自定义数据结构体,顶点着色器输出的数据,也是片元着色器输入数据
{
float4 pos : SV_POSITION; //输出裁剪空间下的顶点坐标数据,给光栅化使用,必须要写的数据
float2 uv : TEXCOORD0; //自定义数据体
//注意跟上方的TEXCOORD的意义是不一样的,上方代表的是UV,这里可以是任意数据。
//插值器:输出后会被光栅化进行插值,而后作为输入数据,进入片元Shader
//最多可以写16个:TEXCOORD0 ~ TEXCOORD15。
float3 normal : TEXCOORD1;
};

/*
Shader内的变量声明,如果跟上面Properties模块内的参数同名,就可以产生链接
*/
sampler2D _MainTex;
float4 _MainTex_ST;
//Unity内置变量:https://docs.unity3d.com/560/Documentation/Manual/SL-UnityShaderVariables.html
//Unity内置函数:https://docs.unity3d.com/560/Documentation/Manual/SL-BuiltinFunctions.html

//顶点Shader
v2f vert (appdata v)
{
v2f o;
//float4 pos_world = mul(unity_ObjectToWorld, v.vertex);
//float4 pos_view = mul(UNITY_MATRIX_V, pos_world);
//float4 pos_clip = mul(UNITY_MATRIX_P, pos_view);
//o.pos = pos_clip;
//o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord0 * _MainTex_ST.xy + _MainTex_ST.zw;
//o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.normal = v.normal;
return o;
}
//片元Shader
half4 frag (v2f i) : SV_Target //SV_Target表示为:片元Shader输出的目标地(渲染目标)
{
half4 col = tex2D(_MainTex, i.uv);
//fixed4 col = float4(i.uv,0.0,0.0);
return col;
}
ENDCG // Shader代码从这里结束
}
}
}

Properties内声明的属性值一部分为Unity序列化用,我们还需要在SubShader——Pass块里面再次声明这些属性。

1
2
3
4
精度标准:
float(32bit) :坐标点
half(16bit) :UV、大部分向量
fixed(8bit) :颜色

注意在v2f结构体中,精度都为float

在Shader代码中,如果顶点着色器向片元传递的是向量float(x,x,x,0.0)最后一位给0,如果顶点着色器向片元传递的是float(x,x,x,1.0)最后一位给1。

Shader代码中,两个多维对象是可以进行乘算的,比如Vector2 * Vector2,多维对象的每一维单独相乘,如果需要点乘或叉乘就使用Dot和Cross

贴图映射

背面剔除

背面剔除可以使用相关的属性声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Properties
{
//...
[Enum(UnityEngine.Rendering.CullMode)]_CullMode("CullMode", float) = 2
}
SubShader
{
Pass
{
Cull [_CullMode]
CGPROGRAM
//...
ENDCG
}
}

uv映射到平面

一般的模型,uv信息存储在TEXCOORD0里,我们把这个信息通过vertex传递给fragment进行映射。但是我们可以不要这个uv信息,我们直接使用顶点坐标vertex : POSITION进行映射,这样就可以让一张贴图直接映射到一个模型的xy平面(或者yz平面、xz平面)

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
struct appdata
{
float4 vertex : POSITION;
half2 uv0 : TEXCOORD0;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float2 pos_uv : TEXCOORD1; //这个用来储存当前顶点的模型空间坐标
};
sampler2D _MainTex;
float4 _MainTex_ST;

v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.uv0 * _MainTex_ST.xy + _MainTex_ST.zw;//获取原模型的uv信息。
o.pos_uv = v.vertex.xy * _MainTex_ST.xy + _MainTex_ST.zw;//使用模型空间坐标代替uv
//o.pos_uv = v.vertex.zy * _MainTex_ST.xy + _MainTex_ST.zw; 映射到zy平面,也就是侧视图
//我们也可以使用世界空间坐标代替uv,这样贴图会按照世界坐标单位进行一定的缩放
//float4 pos_world = mul(unity_ObjectToWorld, v.vertex);
//o.pos_uv = pos_world * _MainTex_ST.xy + _MainTex_ST.zw;
}

half4 frag(v2f i)
{
half4 col = tex2D(_MainTex, i.pos_uv);
return col;
}

纹理没有映射到平面

纹理映射到xy平面

uv不合理导致贴图失真

在模型面数有限的情况下,贴图会因为uv排布不合理导致失真

不是直线

上面的图因为uv不合理导致直线不直

美术需要在模型制作阶段就进行uv检查,包括贴图精度和缩放一致性。

如果遇到上述失真的情况,增加模型的细分可以解决。但是不推荐。也可以在片元着色器中重新计算uv坐标,但是会引发其他问题。

AlphaTest

在片元Shader执行完毕后,获得的片元数据进入输出合并阶段:

Alpha Test —— Stencil Test —— Depth Test —— Blending —— FrameBuffer(帧缓冲区)

利用Alpha Test,某些透明像素就会被扔掉,可以实现某些特效(边缘比较锐利的特效,因为Alpha Test是直接扔掉像素)

在实际Shader编写中,主要使用clip函数来实现Alpha剔除的效果。需要记住这里舍弃的是整个片元,所以我们直接返回颜色。

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
Shader "KerryTA/03ClipShader"
{
Properties
{
_MainColor("MainColor",Color) = (1,1,1,1)
_MainTex("Texture",2D) = "white"{}
_NoiseTex("Noise",2D) = "white"{}
_Cutout("Cutout",Range(-0.1,1.1)) = 0.0 //设置剔除程度
_Speed("Speed",Vector) = (1,1,0,0) //设置速度,xy设置灰度图偏移速度,zw设置噪声图偏移速度
[Enum(UnityEngine.Rendering.CullMode)]_CullMode("CullMode", float) = 2
}

SubShader
{
Tags { "RenderType"="Opaque"}
Pass
{

Cull [_CullMode]

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
half2 texcoord0 : TEXCOORD0;
};

struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};

float4 _MainColor;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NoiseTex;
float4 _NoiseTex_ST;
float _Cutout;
float4 _Speed;

v2f vert (appdata v)
{
v2f o;

o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord0 * _MainTex_ST.xy + _MainTex_ST.zw;
return o;
}

half4 frag (v2f i) : SV_Target
{
half gradient = tex2D(_MainTex,i.uv + _Time.y * _Speed.xy).r; //拾取一个灰度图的r通道
half noise = tex2D(_NoiseTex,i.uv + _Time.y * _Speed.zw).r; //拾取一个噪声图的r通道
clip(gradient - noise - _Cutout); // 剔除掉黑色的片元
return _MainColor;
//return gradient.xxxx // 返回颜色值的简便写法,相当于float4(gradient,gradient,gradient,gradient)
}

ENDCG
}
}
}

时间属性_Time是个四维变量,这里取y。注意使用时间来偏移uv时,在片元代码中修改uv。

Name Type Value
_Time float4 Time since level load (t/20, t, t*2, t*3), use to animate things inside the shaders.
_SinTime float4 Sine of time: (t/8, t/4, t/2, t).
_CosTime float4 Cosine of time: (t/8, t/4, t/2, t).
unity_DeltaTime float4 Delta time: (dt, 1/dt, smoothDt, 1/smoothDt).

Blend

在Pass语义块中CGPROGRAM上面添加BlendZWrite Off,透明物体前后遮挡时必须关闭ZWrite,否则后面的片元都会被舍弃。

在SubShader的Tags中将Queue指定为Transparent

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
Shader "KerryTA/04BlendingShader"
{
Properties
{
_MainTex("Texture",2D) = "white"{}
_MainColor("MainColor",Color) = (1,1,1,1)
_Emiss("Emission",Float) = 1.0
[Enum(UnityEngine.Rendering.CullMode)]_CullMode("CullMode", float) = 2
}

SubShader
{
Tags { "Queue" = "Transparent"}
Pass
{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
//也可以使用相加模式
//Blend SrcAlpha One
Cull [_CullMode]

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
half2 texcoord0 : TEXCOORD0;
};

struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};

sampler2D _MainTex;
float4 _MainTex_ST;
float4 _MainColor;
float _Emiss;

v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord0 * _MainTex_ST.xy + _MainTex_ST.zw;
return o;
}

half4 frag (v2f i) : SV_Target
{
half3 col = _MainColor.xyz * _Emiss;
//将灰度图的r通道转变为alpha值,注意alpha和发光系数_Emiss相乘时不能超过1(否则HDR会出问题),所以使用saturate函数
half alpha = saturate(tex2D(_MainTex,i.uv).r * _MainColor.w * _Emiss);
return half4(col,alpha);
}

ENDCG
}
}
}

边缘光

边缘光的原理就是在片元着色器中求当前片元到摄像机的向量当前片元世界空间下的法向量的点乘(Dot),根据结果舍弃掉一些夹角比较小的片元。

注意顶点函数中求世界空间下的normal时,使用的是normalize(mul(float4(v.normal,0.0), _World2Object).xyz),这是因为法线不能因为模型单个轴的缩放导致偏移,法线有自己的变换矩阵,经过公式推导后我们求世界空间下的法线时就需要法向量右乘世界到模型的矩阵。

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
Shader "KerryTA/05RimShader"
{
Properties
{
//_MainTex("Texture",2D) = "white"{}
_MainColor("MainColor",Color) = (1,1,1,1)
_Emiss("Emission",Float) = 1.0
_RimPower("RimPower",Float) = 1.0
}

SubShader
{
Tags { "Queue" = "Transparent"}
Pass
{
ZWrite Off
Blend SrcAlpha One

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
half2 texcoord0 : TEXCOORD0;
float3 normal : NORMAL;
};

struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal_world : TEXCOORD1;
float3 view_world : TEXCOORD2;
};

//sampler2D _MainTex;
//float4 _MainTex_ST;
float4 _MainColor;
float _Emiss;
float _RimPower;

v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.normal_world = normalize(mul(float4(v.normal,0.0), unity_WorldToObject).xyz);//求当前顶点的世界空间下的法向量
float3 pos_world = mul(unity_ObjectToWorld,v.vertex).xyz;
o.view_world = normalize(_WorldSpaceCameraPos.xyz - pos_world);//求当前顶点到摄像机世界空间下的法向量
//o.uv = v.texcoord0 * _MainTex_ST.xy + _MainTex_ST.zw;
return o;
}

half4 frag (v2f i) : SV_Target
{
float3 normal_world = normalize(i.normal_world);//片元会将顶点进行插值,所以需要重新归一化
float3 view_world = normalize(i.view_world);
//点乘的区间为-1到1,使用saturate将负数归零,相当于去掉成钝角的法向量,注意去不掉反面成锐角的法向量
float NdotV = saturate(dot(normal_world,view_world));
//使用pow函数将结果按指数进行乘方,可以控制边缘光的对比度
float fresnel = pow(1.0 - NdotV, _RimPower);
float alpha = saturate(fresnel * _Emiss);
float3 col = _MainColor.xyz * _Emiss;

return float4(col,alpha);
}

ENDCG
}
}
}

深度预写入

利用上面的Shader制作的边缘光还不够完美,即使我们设置了ZWrite On,在实际界面中我们会发现模型被遮挡的背面依然会渲染。这是因为在同一个模型下,使用ZWrite来去掉背面有一定问题,就是每一个顶点的深度一边计算一边写入,如果后面的顶点比前面的顶点预先渲染,那么前面的顶点就不能遮挡后面的。

想要完美解决这个问题,就需要再加一个Pass,放在边缘光Pass的前面,在这个Pass中,我们预先将所有顶点的深度计算出来。这样就能解决上述问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Pass
{
Cull Off
ZWrite On
ColorMask 0//只写入深度,不写入颜色
CGPROGRAM
float4 _Color;
#pragma vertex vert
#pragma fragment frag

float4 vert(float4 vertexPos : POSITION) : SV_POSITION
{
return UnityObjectToClipPos(vertexPos);
}
float4 frag(void) : COLOR
{
return _Color;
}
ENDCG
}