UnityShader中级

复杂光照

Unity 渲染路径:Forward Deferred Legacy Vertex Lit Legacy Deferred

标签名 描述
Always 不管使用哪种渲染路径,该pass总会被渲染,但不会计算任何光照
ForwardBase 用于前向渲染。该pass会计算环境光,最重要的平行光,逐顶点/SH光源和Lightmaps
ForwardAdd 用于前向渲染。该pass会计算额外的逐像素光源,每个pass对应一个光源。
Deferred 用于延迟渲染。该pass会渲染G缓冲(G-buffer)
ShadowCaster 把物体的深度信息渲染到阴影映射纹理或一张深度纹理中
PrepassBase 用于遗留的延迟渲染。该pass会渲染法线和高光反射的指数部分。
PrepassFinal 用于遗留的延迟渲染。该pass会通过合并纹理,光照和自发光来渲染得到最后颜色
Vertex, VertexLMRGBM和VertexLM 用于遗留的顶点照明渲染

在Shader对光照渲染模式进行设置

1
2
//在Shader指定光照渲染模式
Tags{"LightMode"= "ForwardBase"}

注意:用空格进行不同标签的分割

ForwardBase向下兼容Deferred,但是不支持阴影

不同标签互不兼容

前向渲染

Unity 前向渲染:逐顶点,逐像素处理,球谐函数。

image-20241104160530282

两种光源的区分

  • 逐像素光源,光照更细腻,对性能要求更高,会对Shader的计算产生影响

就像是在顶点着色器中计算颜色一样,锯齿状的边缘会非常明显

多个平行光,挑一个最重要的来进行计算,如果剩下的平行光数量满足在 质量 中 对逐像素 光源数量的限制,并且在光源的RenderMode中设置的是Auto,那么剩下的平行就会自动成为 逐像素光源,然后放到ForwardAdd里面去计算,超过限制就成为逐顶点光源

  • 逐像素光源:
  • 1,当光源设置为Import时,是逐像素光源。(不受限制与质量设置里面pixel light count )(Forward Add)
  • 2,光源为auto时,个数在pixel light count 以内的光源都是逐像素光源。(Forward Add)
  • 3,光源为auto时,个数超过pixel light count ,那么按光源对物体影响重要程度排序后,前pixel light count个数的光源为逐像素光源。(Forward Add)
  • 4,最重要的平行光为逐像素光源。(Forward Base)
  • 逐顶点光源:(unity默认要求逐顶点光源不超过4个,超过的按SH光源处理,也就是球协函数)
  • 1,当光源设置为NotImport时,是逐顶点光源。(Forward Base)
  • 2,超过pixel light count 的光源为逐顶点光源。(Forward Base)
image-20241104162623187 image-20241104162522370

逐顶点光源计算

逐顶点光源计算中函数用到的参数可以在下载到的对应shader文件 UnityShaderVariables.cginc 中查找到

单纯在一个没有FallBack,仅仅实现了一个ForWardBase的Shader中进行计算,只会有一次DrawCall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifdef LIGHTMAP_OFF
//球协函数计算
float3 shLight = ShadeSH9(float4(v.normal,1.0));
o.vertexLight = shLight;
#ifdef VERTEXLIGHT_ON
//逐顶点光源计算
//光源设定,unity_LightColor0/1/2/3
float3 vertexLight = Shade4PointLights(unity_4LightPosX0,unity_4LightPosY0,unity_4LightPosZ0,

unity_LightColor0.rgb,unity_LightColor1.rgb,unity_LightColor2.rgb,unity_LightColor3.rgb,
unity_4LightAtten0,
o.worldNormal,o.worldNormal);
o.vertexLight += vertexLight;
#endif
#endif

每次Pass通道运行的时候回默认获取当前光照的颜色

1
_LightColor0.rgb
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
Shader "Unlit/11"
{
Properties
{
_Diffuse("Diffuse",Color) =(1,1,1,1)
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(8.0,256)) = 20

}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{

Tags{"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog

#include "UnityCG.cginc"
#include "UnityLightingCommon.cginc"


struct v2f
{
float4 vertex : SV_POSITION;
float3 worldNormal :TEXCOORD0;
float3 worldPos : TEXCOORD1;
float3 vertexLight : TEXCOORD2;
};

sampler2D _MainTex;
float4 _MainTex_ST;

float4 _Diffuse;
float4 _Specular;
float _Gloss;

v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
#ifdef LIGHTMAP_OFF
//球协函数计算
float3 shLight = ShadeSH9(float4(v.normal,1.0));
o.vertexLight = shLight;
#ifdef VERTEXLIGHT_ON
//逐顶点光源计算
float3 vertexLight = Shade4PointLights(unity_4LightPosX0,unity_4LightPosY0,unity_4LightPosZ0,
unity_LightColor0.rgb,unity_LightColor1.rgb,unity_LightColor2.rgb,unity_LightColor3.rgb,
unity_4LightAtten0,
o.worldNormal,o.worldNormal);
o.vertexLight += vertexLight;
#endif
#endif

return o;
}

fixed4 frag (v2f i) : SV_Target
{
float3 environmentLightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
//世界法线标准化
//视角方向计算
//光线方向计算
float3 normal = normalize(i.worldNormal);
float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
float3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

//满发射光线计算
float3 diffuseColor = _LightColor0.rgb * _Diffuse.rgb * (dot(lightDir,normal) * 0.5 + 0.5);

//高光反射计算
float3 halfView = normalize(lightDir + viewDir);
float3 specularColor = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(normal,halfView)),_Gloss); //高光反射还需要继续计算

//可以将高光颜色和漫反射颜色乘上一个衰减系数
//不过平行光不会衰减
//也可以在后面加上逐顶点着色的颜色
float3 resColor = environmentLightColor + (specularColor + diffuseColor) + i.vertexLight;

return float4(resColor,1);

}
ENDCG
}

Pass
{
Tags{"LightMode" = "ForwardAdd"}


Blend One One

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#pragma multi_compile_fwdadd

#include "UnityCG.cginc"

//包含光照衰减的方法
#include "AutoLight.cginc"
#include "UnityLightingCommon.cginc"

struct v2f
{
float4 vertex : SV_POSITION;
float3 worldNormal :TEXCOORD0;
float3 worldPos : TEXCOORD1;
LIGHTING_COORDS(2,3)//使用 TEXCOORD2 和 3的衰减光,两个参数值,一个衰减,一个阴影
};



float4 _Diffuse;
float4 _Specular;
float _Gloss;

v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;

//光照衰减
TRANSFER_VERTEX_TO_FRAGMENT(o);

return o;
}

fixed4 frag (v2f i) : SV_Target
{
float3 environmentLightColor = UNITY_LIGHTMODEL_AMBIENT.xyz;
//世界法线标准化
//视角方向计算
//光线方向计算
float3 normal = normalize(i.worldNormal);
float3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
float3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));

//满发射光线计算
float3 diffuseColor = _LightColor0.rgb * _Diffuse.rgb * (dot(lightDir,normal) * 0.5 + 0.5);

//高光反射计算
float3 halfView = normalize(lightDir + viewDir);
float3 specularColor = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(normal,halfView)),_Gloss); //高光反射还需要继续计算

//衰减
float atten = LIGHT_ATTENUATION(i);
//可以将高光颜色和漫反射颜色乘上一个衰减系数
//不过平行光不会衰减
//也可以在后面加上逐顶点着色的颜色
float3 resColor = environmentLightColor + (specularColor + diffuseColor) * atten;

return float4(resColor,1);

}

ENDCG

}
}

}

前向渲染补充

  • 1,ForwardAdd这个Pass需要和ForwardBase一起使用,否则会被Unity忽略掉(unity5.x),在新版本中,不会忽略,但是渲染会出错。
  • 2,在Forward和Deferred渲染路径下,Forward的Pass均能被正常渲染。
  • 3,ForwardAdd对每一个逐像素光源都会执行一次Pass,所以不要在ForwardAdd里面计算 unity_4LightPos[x,y,z]0中的数据。会重复计算。

在Frame Debugger,中的其他光源排序,按照光源的强度,大小和距离进行排序

延迟渲染

延迟渲染原理:

延迟渲染主要包含了两个Pass。在第一个Pass中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,这主要是通过深度缓冲技术来实现,当发现一个片元是可见的,我们就把它的相关信息存储到G缓冲区中。然后,在第二个Pass中,我们利用G缓冲区的各个片元信息,例如表面法线、视角方向、漫发射系数等,进行真正的光照计算。

延迟渲染缺点:

1,不支持真正的抗锯齿功能。

2,不能处理半透明物体。

3,对显卡有一定要求。如果要使用延迟渲染的话,显卡必须支持MRT、Shader Mode 3.0及以上、深度渲染纹理以及双面的模板缓冲。

延迟渲染优点:

1,所有光都是逐像素光源。计算复杂度前向渲染 O(m*n),延迟渲染O(m+n)。

2,制作后处理等,可直接获取深度值。

1)第一个Pass用于渲染G缓冲。在这个Pass中,我们会把物体的漫反射颜色、高光发射颜色、平滑度、法线、自发光和深度等信息渲染到屏幕空间的G缓冲区中。对于每个物体来说,这个Pass仅会执行一次。

2)第二个Pass用于计算真正的光照模型。这个Pass会使用上一个Pass中渲染的数据来计算最终的光照颜色,再存储到帧缓冲中。

G缓冲

默认的G缓冲区(注意,不同Unity版本的渲染纹理存储内容会有所不同)包含了以下几个渲染纹理(Render Texture,RT)。

RT0:格式是ARGB32(每个通道8位),RGB通道用于存储漫反射颜色,A通道储存遮挡。

RT1:格式是ARGB32(每个通道8位),RGB通道用于存储高光反射颜色,A通道同于用于存储高光反射的指数部分。

RT2:格式是ARGB2101010,RGB通道用于存储世界空间法线,A通道没有被使用。

RT3:格式是ARGB2101010/ARGBHalf(每个通道16位),(低动态光照渲染/高动态光照渲染)用于存储自发光+lightmap+反射探针深度缓冲和模板缓冲。

当在第二个Pass中计算光照时,默认情况下仅可以使用Unity内置的Standard 光照模型。

image-20241105103522669

实现

第一个Pass

在没有自定义Deferred第二个Pass的情况下,片元着色器frag中输出的每个元素都要按照给定的顺序,漫反射光,高光,法线,深度缓冲

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
//高光反射,系数
_Gloss("Gloss",Range(8.0,256)) = 8


//延迟渲染输出结构体
struct DeferredOutData
{
float4 gBuffer0 : SV_TARGET0;
float4 gBuffer1 : SV_TARGET1;
float4 gBuffer2 : SV_TARGET2;
float4 gBuffer3 : SV_TARGET3;

};

sampler2D _MainTex;
float4 _MainTex_ST;

float4 _Diffuse;
float4 _Specular;
float _Gloss;

v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}

DeferredOutData frag (v2f i)
{
DeferredOutData d;
float3 color = tex2D(_MainTex,i.uv).rgb * _Diffuse.rgb;
//漫反射颜色
d.gBuffer0.rgb = color;
d.gBuffer0.a =1;

//高光反射,a通道存储高光反射的幂
d.gBuffer1.rgb = _Specular.rgb;
d.gBuffer1.a = _Gloss /256; //a通取值范围的问题,要卡在0到1之间,除的数要和该数在Properties中的最大值相同

//法线
d.gBuffer2 = float4(i.normal *0.5 + 0.5); //同样是a通道的取值范围问题
d.gBuffer3 = float4(color,1); //存储模板缓冲,也就是计算出来的颜色值
return d;
}

在相机中出现需要进行的设置

image-20241105111442328

屏幕后处理(第二个Pass)

就是延迟渲染的第二Pass,不需要进行深度写入

方法

将给定的裁剪空间坐标转换为屏幕空间坐标

1
2
3
4
5
6
7
inline float4 ComputeScreenPos(float4 pos) {
float4 o = ComputeNonStereoScreenPos(pos);
#if defined(UNITY_SINGLE_PASS_STEREO)
o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
#endif
return o;
}

在顶点着色器中使用,结果还不一个完整的屏幕空间坐标,需要后续在片元着色器中用xy分量除以zw分量(齐次除法)

投影空间不是线性的,插值是线性的

这个方法按照习惯会在顶点着色器中被调用,如果方法帮我们进行齐次除法,最后插值的结果就不会很准确,因此需要我们手动去片元着色器中操作

1
o.uv = ComputeScreenPos(o.vertex);

这一步之后还有很多操作需要被进行,这个时候拿到的uv是在屏幕空间的像素,延迟渲染的屏幕后处理进行的打光操作,打光操作需要在世界空间进行,因此需要把uv转换到齐次裁剪空间,再从齐次裁剪空间到世界空间

在第一次使用过程中,引入头文件的过程缺少一个 “ 结尾的标点符号,导致两个识别不出来两个Pass

1
2
3
#include "UnityCG.cginc"
#include "UnityDeferredLibrary.cginc"
#include "UnityGBuffer.cginc

完整代码

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
Shader "Unlit/12-Deferred"
{
Properties
{

}
SubShader
{
//最开始放错了地方,
//有外面的内部的就无效
//没外面的里面就近
//ZWrite Off
//LDR 地动态 HDR 高动态
//LDR Blend DstColor Zero HDR : Blend One One
//Blend one one
Pass
{
ZWrite Off
//LDR 地动态 HDR 高动态
//LDR Blend DstColor Zero HDR : Blend One One
Blend [_SrcBlend] [_DstBlend]
CGPROGRAM
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_lightpass
//代表排除不支持MRT的硬件
#pragma exclude_renderers nomrt
//#pragma multi_compile __ UNITY_HDR_ON

#include "UnityCG.cginc"
#include "UnityDeferredLibrary.cginc"
#include "UnityGBuffer.cginc"

sampler2D _CameraGBufferTexture0;
sampler2D _CameraGBufferTexture1;
sampler2D _CameraGBufferTexture2;
sampler2D _CameraGBufferTexture3;

struct a2v
{
float4 vertex : POSITION;
float3 normal :NORMAL;
};



unity_v2f_deferred vert (a2v i)
{
unity_v2f_deferred o;
o.pos = UnityObjectToClipPos(i.vertex);
o.uv = ComputeScreenPos(o.pos);
o.ray = UnityObjectToViewPos(i.vertex);
//_LightAsQuad 当在处理四边形时,也就是直射光时返回1,否则返回0
o.ray = lerp(o.ray, i.normal, _LightAsQuad);
return o;

}

float4 frag (unity_v2f_deferred i) : SV_Target
{
//不去自己进行手动计算
//需要使用UnityDeferredLibrary.cgnic包中的一个结构体 unity_v2f_deferred
// float3 worldPos;
// float2 uv;
// half3 lightDir;
// float atten;
// float fadeDist;
// UnityDeferredCalculateLightParams(i,worldPos,uv,lightDir,atten,fadeDist);

float2 uv = i.uv.xy / i.uv.w;

//通过深度和方向重新构建世界坐标
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,uv);

//将取得的深度值变化到线性的区间内
depth = Linear01Depth(depth);

//ray只能表示方向,长度不一定,_ProjectionParams.z表示远平面
//括号内计算出来的就是rayToFarP和ray向量的比例
float3 rayToFarPlane = i.ray * (_ProjectionParams.z / i.ray.z);
float4 viewPos = float4(rayToFarPlane * depth,1);
float3 worldPos = mul(unity_CameraToWorld,viewPos).xyz;
//从这到上的整个过程是拿到一个存储在屏幕空间的图,然后转换到世界坐标下

//阴影消失的地方
float fadeDist = UnityComputeShadowFadeDistance(worldPos,viewPos.z);



//对不同光进行衰减计算,包括阴影计算
//从上到下依次是,区域光,方向光,点光
//虽然spot区域光没有cookie的定义,但是在计算的时候还是要加入的
//2019版
#if defined(SPOT)
float3 toLight = _LightPos.xyz - worldPos;
half3 lightDir = normalize(toLight);

float4 uvCookie = mul(unity_WorldToLight,float4(worldPos,1));

//阴影的实时衰减计算消耗大,一般都是存储在_LightTexture0中,如果使用了cookie,就会存储在_LightTextureB0中
float atten = tex2Dbias(_LightTexture0,float4(uvCookie.xy / uvCookie.w,0,-8)).w;

atten *= uvCookie.w < 0;
atten *= tex2D(_LightTextureB0,dot(toLight,toLight) * _LightPos.w).r;

atten *= UnityDeferredComputeShadow(worldPos,fadeDist,uv);
#elif defined(DIRECTIONAL) || defined(DIRECTIONAL_COOKIE)
half3 lightDir = -_LightDir.xyz; //关键性位置
float atten = 1.0;

atten *= UnityDeferredComputeShadow(worldPos,fadeDist,uv);

#if defined(DIRECTIONAL_COOKIE)
float4 uvCookie = mul(unity_WorldToLight,float4(worldPos,1));

//方向光不透视,因此不需要进行齐次裁剪
atten *= tex2Dbias(_LightTexture0,float4(uvCookie.xy,0,-8)).w;
#endif
#elif defined(POINT) || defined(POINT_COOKIE)
float3 toLight = _LightPos.xyz - worldPos;
half3 lightDir = normalize(toLight);

float atten = tex2D(_LightTextureB0,dot(toLight,toLight) * _LightPos.w).r;
atten *= UnityDeferredComputeShadow(worldPos,fadeDist,uv);

#if defined(POINT_COOKIE)
float4 uvCookie = mul(unity_WorldToLight,float4(worldPos,1));
atten *= texCUBEbias(_LightTexture0,float4(uvCookie.xyz,-8)).w;

#endif
#else
half3 lightDir = 0;
float atten = 0;
#endif

half3 lightColor = _LightColor.rgb * atten;

half4 gbuffer0 = tex2D(_CameraGBufferTexture0, uv);
half4 gbuffer1 = tex2D(_CameraGBufferTexture1, uv);
half4 gbuffer2 = tex2D(_CameraGBufferTexture2, uv);

half3 diffuseColor = gbuffer0.rgb;
half3 specularColor = gbuffer1.rgb;
float gloss = gbuffer1.a * 256;
float3 worldNormal = normalize(gbuffer2.xyz * 2 - 1);

float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float3 halfView = normalize(viewDir + lightDir);

half3 diffuse = lightColor * diffuseColor * (dot(worldNormal,lightDir) * 0.5 + 0.5);
half3 specular = lightColor * specularColor * pow(max(0,dot(worldNormal,halfView)),gloss);

return float4(diffuse + specular,1);

}
ENDCG
}


//转码pass,主要是对于LDR转码
Pass
{
ZTest Always
Cull Off
Zwrite Off
Stencil
{
ref[_stencilNonBackground]
readMask[_StencilNonBackground]
compback equal
compfront equal
}

CGPROGRAM

//要求Shader版本子在3.0以上
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag

//排除不支持MRT的硬件
#pragma exclude_renderers nomrt

#include "UnityCG.cginc"

sampler2D _LightBuffer;

struct v2f{
float4 vertex : SV_POSITION;
float2 texcoord : TEXCOORD0;
};

v2f vert(float4 vertex : POSITION,float2 texcoord : TEXCOORD0)
{
v2f o;
o.vertex = UnityObjectToClipPos(vertex);
o.texcoord.xy = texcoord;

//某个不知名平台的宏
#ifdef UNITY_SINGLE_PASS_STEREO
o.texcoord = TransformStereoScreenSpaceTex(o.texcoord, 1.0);

#endif
return o;
}

float4 frag(v2f i):SV_Target
{
//针对某个解码?
//猜测可能高动态和低动态的差别
return -log2(tex2D(_LightBuffer,i.texcoord));
}
ENDCG
}
}
}


image-20241105200158797

1
2
3
4
5
6
7
8
   //不去自己进行手动计算
//需要使用UnityDeferredLibrary.cgnic包中的一个结构体 unity_v2f_deferred
float3 worldPos;
float2 uv;
half3 lightDir;
float atten;
float fadeDist;
UnityDeferredCalculateLightParams(i,worldPos,uv,lightDir,atten,fadeDist);

这样的操作需要的前置cginc头文件是

1
#include "UnityDeferredLibrary.cginc"

模板测试

常用场景

通常在屏幕后处理和UI的Shader中用的比较多

需求场景

如果一个Shader本身有自己的深度需求和透明需求,这个时候还要控制某些东西渲染,某些东西不渲染,或者在某个区域渲染,在某个区域不渲染,这样的一个功能需求的时候,就会用到模板测试

说白了还是根据给出的条件来判断某个片元是否应该抛弃

概念

Stencil (模板测试/蒙版测试):

与深度测试,透明度测试类似,决定一个片元是否被扔掉。深度测试的比较数据在深度缓冲中,透明度测试的比较对象是颜色缓冲中的值,而模版测试的比较数据在Stencil中,并且模板测试要先于深度测试与透明度测试,在fragment函数之前就会执行模板测试。

Ref 就是参考值,当参数允许赋值时,会把参考值赋给当前像素

ReadMask 对当前参考值和已有值进行mask操作,默认值255,一般不用

WriteMask 写入Mask操作,默认值255,一般不用

Comp 比较方法。是拿Ref参考值和当前像素缓存上的值进行比较。默认值Always

Pass 当模版测试和深度测试都通过时,进行处理

Fail 当模版测试和深度测试都失败时,进行处理

ZFail 当模版测试通过而深度测试失败时,进行处理

Comp :

  1. Always
  2. Greater - 大于
  3. GEqual - 大于等于
  4. Less - 小于
  5. LEqual - 小于等于
  6. Equal - 等于
  7. NotEqual - 不等于
  8. Always - 永远通过
  9. Never - 永远通不过

pass,Fail,ZFail:

  1. Keep 保持(即不把参考值赋上去,直接不管)
  2. Zero 归零
  3. Replace 替换(拿参考值替代原有值)
  4. IncrSat 值增加1,但不溢出,如果到255,就不再加
  5. DecrSat 值减少1,但不溢出,值到0就不再减
  6. Invert 反转所有位,如果1就会变成254
  7. IncrWrap 值增加1,会溢出,所以255变成0
  8. DecrWrap 值减少1,会溢出,所以0变成255

案例

写在Pass里面的不能被Unity识别,至少在设置窗口没显示

1
Tags{"LightMode"="ForwardBase" "Queue"="Geometry-1999"}

根据摄像机对物体的渲染顺序进行模板缓冲写入

根据自定义的 参考值 _Stencil_Ref,以及配合一下代码,实现反深度测试的效果

1
2
3
4
5
6
7
8
9
ZTest Off
Stencil
{
Ref[_Stencil_Ref]

Comp GEqual //大于等于
Pass Replace //替换原有值 ,初始值为0,替换掉的是上一个参考值

}

一个材质和Shader可以确定唯一的表现效果

可以使用多个材质配合一个Shader来实现Shader复用

根据渲染顺序来进行模板缓冲写入

充当背景的物体设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_refVal("refVal",int) = 1
}
SubShader
{
Tags { "LightMode" = "ForwardBase" "Queue" = "Geometry" }
LOD 100

Pass
{
Stencil
{
Ref[_refVal]

Pass Replace
Fail Replace
ZFail Replace
}

需要穿透的物体设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_refVal("refVal",int) = 1
}
SubShader
{
Tags { "LightMode" = "ForwardBase" "Queue" = "Geometry+2" }
LOD 100

Pass
{
Stencil
{
Ref[_refVal]
Comp GEqual
Pass Replace
}

穿透物体设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Properties
{
_MainTex ("Texture", 2D) = "white" {}

_refVal("refVal",int) = 1
}
SubShader
{
Tags { "LightMode" = "ForwardBase" "Queue" = "Geometry+1" }
ColorMask 0 //RBGA,设置那个就输出对应的颜色通道值,0就代表不输出颜色
LOD 100

Pass
{
Stencil
{
Ref[_refVal]
Comp GEqual
Pass Replace
}

image-20241106191951377

注意:在透视投影的情况下,这种形式下的挖洞需要有单独的背景或者游戏对象去支持,单纯的天空盒会是黑色

image-20241106192127579

image-20241106192218959

区域蒙版

实践效果参照Unity的UI组件Mask的效果,在给定区域内才能正常显示,超出范围会被裁剪

image-20241107093146161

作为背景Stencil设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_refVal("RefVal",int) =1
}
SubShader
{
Tags { "LightMode" = "ForwardBase" "Queue"= "Geometry"}
LOD 100

Pass
{
Stencil
{
Ref[_refVal]
Comp Always //总是将该参考值写入缓冲
Pass Replace
}

需要被裁剪的Stencil设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Properties
{
_MainTex ("Texture", 2D) = "white" {}

_refVal("RefVal",int) =1
}
SubShader
{
Tags { "LightMode" = "ForwardBase" "Queue"= "Geometry+1"}

LOD 100

Pass
{
Stencil
{
Ref[_refVal]
Comp Equal //只有当当前参考值和上一步相等的时候才会被渲染
}

光照衰减

Unity在内部使用一张名为_LightTexture0的纹理来计算光源衰减。我们通常只关心_LightTexture0对角线上的纹理颜色值,这些值表明了再光源空间中不同位置的点的衰减值。例如(0,0)点表明了与光源位置重合的点的衰减值,而(1,1)点表明了再光源空间中所关心的距离最远的点的衰减。

1
2
3
float3 lightCoord = mul(_LightMatrix0,float4(i.worldPos,1)).xyz; 

fixed atten = tex2D(_LightTexture0,dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;

现将世界坐标与_LightMatrix0相乘得到在光源空间中的位置,用光源空间中顶点距离的平方来对纹理采样,然后,使用宏UINITY_ATTEN_CHANNEL来得到衰减纹理中的衰减值所在的分量,以得到最终的衰减值。

数学公式计算光照衰减

1
2
3
float distance = length(_WorldSpaceLightPos0.xyz - i.worldPosition.xyz); 

atten = 1.0/distance;

对于点光源和方向光可以简单用这个来进行计算,区域光就不能使用,区域光的各个参数不可控制,一般来说都是使用Unity中自带的

阴影映射原理

Unity中阴影

1,Shadow Map:它会首先把摄像机位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是摄像机看不到的地方。

摄像机放置到光源位置上就会产生一个深度映射纹理,根据这个深度图区判断一个点是不是在阴影内

2,Screenspace Shadow Map:Unity首先会通过调用LightMode 为 ShadowCaster的Pass来得到可投射阴影是光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却出于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需要在Shader中对阴影图进行采样。

总结来说就是分别在光源位置和摄像机位置产生一张阴影采样图,通过这两张图得到一个屏幕空间的阴影图。

阴影采样图的产生

1
Tags{"LightMode" = "ShadowCaster"}

就可以,一般在FallBack中就会存在一个阴影的Pass,不需要特别去写

一个物体接收来自其他物体的阴影,以及它向其他物体投射阴影是两个过程。

1,如果我们想要一个物体接收来自其他物体的阴影,就必须在Shader中对阴影映射纹理(包括屏幕空间的阴影图)进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。

2,如果我们想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。在Unity 中,这个过程通过为该物体执行LightMode 为ShadowCaster 的 Pass 来实现的。如果使用了屏幕空间的投射映射技术,Unity还会使用这个Pass 产生一张摄像机的深度纹理。

阴影产生

image-20241107102313002

实现产生阴影的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
 Pass {
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#pragma multi_compile_shadowcaster
#pragma multi_compile_instancing // allow instanced shadow pass for most of the shaders
#include "UnityCG.cginc"

struct v2f {
V2F_SHADOW_CASTER;
UNITY_VERTEX_OUTPUT_STEREO
};

v2f vert( appdata_base v )
{
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}

float4 frag( v2f i ) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i)
}
ENDCG

}
image-20241107104041223

接受阴影

前向渲染手动配置接受阴影

头文件

1
#include "AutoLight.cginc"

v2f结构体

1
2
3
4
5
6
7
8
struct v2f
{
float4 pos : SV_POSITION; //使用Unity定义的阴影宏,这个就需要定义为pos
float3 worldNormal :TEXCOORD0;
float3 worldPos : TEXCOORD1;
float3 vertexLight : TEXCOORD2;
SHADOW_COORDS(3) //接受阴影需要用到的宏,传入的参数是分配的TEXCOORD的索引
};

vert内容

需要放在顶点着色器最后面

1
2
TRANSFER_SHADOW(o);
return o;

frag内容

1
2
3
4
5
6
7
8
9
//fixed shadow = SHADOW_ATTENUATION(i);
//这个宏同时包含了光照衰减和阴影,atten在宏中被定义和赋值,这不需要定义
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
//可以将高光颜色和漫反射颜色乘上一个衰减系数
//不过平行光不会衰减
//也可以在后面加上逐顶点着色的颜色
float3 resColor = environmentLightColor + (specularColor + diffuseColor) * atten;

return float4(resColor,1);
image-20241107110759451 image-20241107145319588

AlphaTest下的阴影

前置属性设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Properties
{
_MainTex("MainTex",2D) = "white"{}

//本身是慢翻身颜色,但是在FallBack的方法中的定义名称为 _Color,所以这里要求同步
_Color("Diffuse",Color) =(1,1,1,1)
_Specular("Specular",Color) = (1,1,1,1)
_Gloss("Gloss",Range(8.0,256)) = 20

//同理还有这个也需要同步
_Cutoff("CutOut",Range(0,1)) = 0.1

}
SubShader
{
Tags { "RenderType"="TransparentCutOut" "IgnoreProjector"="True" "Queue" = "AlphaTest" }
LOD 100

在片元着色器中的输出格式

片元裁剪

1
2
3
4
5
6
7
//uv计算

float4 texColor = tex2D(_MainTex,i.uv);
clip(texColor.a - _Cutoff);//如果传递的值小于零,那么当前片元就会被裁剪掉,相当于普通的ifelse

//漫反射光线计算
float3 diffuseColor = _LightColor0.rgb * _Color.rgb * (dot(lightDir,normal) * 0.5 + 0.5) * texColor;

混合输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//fixed shadow = SHADOW_ATTENUATION(i);

//这个函数同时包含了光照衰减和阴影,atten在宏中被定义和赋值,这不需要定义

UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
//可以将高光颜色和漫反射颜色乘上一个衰减系数

//不过平行光不会衰减

//也可以在后面加上逐顶点着色的颜色

//光照衰减只有漫反射光和高光反射会有,环境光和球协函数不能有
float3 resColor = environmentLightColor + (diffuseColor + specularColor) * atten+ i.vertexLight;


return float4(resColor,1);
image-20241107165816713

AlphaBlend阴影

Tags设置

1
Tags { "Queue" = "Transparent" "RenderType" = "Transparent" "IgnoreProjector" = "true"}
1
2
3
Tags{"LightMode" = "ForwardBase"}
ZWrite Off //半透明物体需要取消深度吸入,但同时,阴影的计算会出现问题,因此,半透明物体是不会进行阴影计算的
Blend SrcAlpha OneMinusSrcAlpha

可以让透明物体强制投射影子和接受影子

投射影子

1
FallBack "Diffuse"

接受影子

将渲染顺序限制在AlphaTest和Transparent之间