求知若饥,虚心若愚
Unity的渲染路径
渲染路径(Rendering Path) 决定了光照是如何应用到UnityShader中的。要为每个Pass指定它使用的渲染路径,该Shader的光照计算才能被正确执行。
在Unity5.0之前,有3中渲染路径(现在一般也是,最近有个Forward+):
- 前向渲染路径(Forward Rendering Path)
- 延迟渲染路径(Deferred Rendering Path)
- 顶点照明渲染路径(Vertex Lit Rendering Path)
一般项目只使用一种渲染路径,可以这样设置默认的渲染路径:Edit->Graphics->Tier Settings->…->Rendering Path(在旧版本的Unity中,Edit->Project Settings->Player->Other Settings->Rendering Path)。
也可以在Camera中设置来覆盖默认渲染路径(默认是使用Project Setting的值),这为使用多种渲染路径提供可能。需要注意的是,如果当前显卡不支持所选择的渲染路径
,Unity会自动使用更低一级的渲染路径。例如GPU不支持延迟渲染,那么就会使用前向渲染路径。
完成设置后还需要在Shader中每个Pass使用标签来指定该Pass使用的渲染路径。
1 | Pass { |
LightMode标签支持的渲染路径设置选项
标签名 | 描述
---|---
Always | 不管使用哪种渲染路径,该Pass总是会被渲染,但不会计算任何光照
ForwardBase | 用于前向渲染,该Pass会计算环境光、最重要的平行光(没有重要的则为黑色),逐顶点/SH光源和Lightmaps
ForwardAdd | 用于前向渲染,该Pass会计算额外的逐像素光源(最重要的设置项个数光源),每个Pass对应一个光源
Deferred | 用于延迟渲染,该Pass会渲染G缓冲(G-buffer)
ShadowCaster | 把物体的深度信息渲染到阴影映射纹理(shadermap)或一张深度纹理中
PrepassBase | 用于遗留的延迟渲染,该Pass会渲染法线和高光反射的指数部分
PrepassFinal | 用于遗留的延迟渲染,该Pass通过合并纹理、光照和自发光来渲染得到最后的颜色
Vertex、VertexLMRGBM 和 VertexLM | 用于遗留的顶点照明渲染
通过设置渲染路径,Unity才能正确对相关的光照属性赋值,是和底层渲染引擎的一次重要的沟通。
前向渲染路径
-
前向渲染路径的原理
渲染该对象的渲染图元,并计算颜色缓冲区和深度缓冲区的信息。大致过程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Pass{
for(each primitive in this model){
for(each fragment covered by this primitive){
if(failed in depth test){
// 如果没有通过深度测试,说明该片元是不可见的
discard;
}else{
// 如果该片元可见,就进行光照计算
float4 color=Shading(materialInfo,pos,normal,lightDir,viewDir);
// 更新帧缓冲
writeFrameBuffer(fragment,color);
}
}
}
}对每个逐像素光源,都需要进行上面一次完整的渲染流程。假设场景中有N个物体,每个物体受M个光源的影响,一个需要N*M个Pass。 -
Unity中的前向渲染
在Unity中前向渲染路径有3种处理光照(照亮物体)的方式:- 逐顶点处理
- 逐像素处理
- 球谐函数处理(Spherical Harmonics, SH)
决定一个光源使用哪种处理模式,取决于它的类型和渲染模式。渲染模式指光源是否是重要的(Important),如果是重要的,就会进行逐像素光源处理。
设置光源的类型和渲染模式
前向渲染中,Unity会根据场景中各个光源的设置以及这些光源对物体的影响程度(例如,距离物体的远近、光源强度等)对这些光源进行重要度排序。其中一定数目的光源按逐像素处理,最多4个光源按逐顶点处理,剩下的可以按SH处理。规则如下:
- 场景中最亮的平行光总是按逐像素处理
- 渲染模式被设置成NotImportant的光源,逐顶点或SH
- 渲染模式被设置成Important的光源,逐像素
- 如果根据以上规则得到的逐像素光源数量小于QualitySetting中的逐像素光源数量(Pixel Light Count),会有更多的光源逐像素处理
BasePass和Additional Pass进行的标签和渲染设置以及常规光照计算如下
补充说明:
- pragma multi_compile_fwdbase和#pargma multi_compile_fwdadd。使用了这两个编译指令,才可以在相关Pass中得到正确的光照变量,如光照衰减值
- BasePass旁边的注释给出一些支持的光照特性
- BasePass中渲染的平行光默认是支持阴影的,AdditionalPass中默认是没阴影的(可以通过multi_compile_fwdadd_fullshadows打开,为点光源和聚光灯提供阴影效果,但需要Unity内部使用更多的Shader变种)
- 环境光和自发光也是在BasePass中计算的,不在AdditionalPass中计算是为了防止叠加多次环境光和自发光
- AdditionalPass开启和设置了混合模式,最终得到多个光照的渲染结果
- 对前向渲染,一个UnityShader通常定义一个BasePass和一个AdditionalPass。前者仅执行一次(双面渲染等多个情况除外),后者会根据影响该物体的其他逐像素光源数目被多次调用
-
内置的光照变量和函数
对于前向渲染(LightMode为ForwardBase或ForwardAdd),可以在Shader中访问到的光照变量:名称 类型 描述 _LightColor0 float4 该Pass处理的逐像素光源的颜色 _WorldSpaceLightPos0 float4 _WorldSpaceLightPos0.xyz 是该Pass处理的逐像素光源的位置。如果该光源是平行光,那么_WorldSpaceLightPos0.w是0,其他光源类型w值为1 _LightMatrix0(新版本是unity_WorldToLight) float4X4 从世界空间到光源空间的变换矩阵。可以用于采样cookie和光强衰减(attenuation)纹理 unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0 float4 仅用于BasePass,前4个非重要的点光源在世界空间中的位置 unity_4LightAtten0 float4 仅用于BasePass,储存了前4个非重要的点光源的衰减因子 unity_LightColor half4[4] 仅用于BasePass,储存了前4个非重要的点光源的颜色 前向渲染可以使用的内置光照函数:函数名 描述 float3 WorldSpaceLightDir(float4 v) 仅可用于前向渲染中。 输入一个模型空间中的顶点位置,返回世界空间中从该点到光源的光照方向。内部实现使用了UnityWorldSpaceLightDir函数。没有被归一化 float3 UnityWorldSpaceLightDir(float4 v) 仅可用于前向渲染中。 输入一个世界空间中的顶点位置,返回世界空间中从该点到光源的光照方向。没有被归一化 float3 ObjSpaceLightDir(float4 v) 仅可用于前向渲染中。 输入一个模型空间中的顶点位置,返回模型空间中从该点到光源的光照方向。没有被归一化 float3 Shade4PointLights(…) 仅可用于前向渲染中。 计算四个点光源的光照,它的参数是已经打包进矢量的光照数据,通常就是上表的内置变量,如unity_4LightPosX0,unity_4LightPosY0,unity_4LightPosZ0,unity_LightColor和unity_4LightAtten0等。前向渲染通常会使用这个函数来计算逐顶点光照
顶点照明渲染路径
对硬件配置要求最少、运算性能最高,但同时也是效果最差的类型,不支持那些逐像素才能得到的效果,例如阴影、法线映射、高精度的高光反射等。
-
Unity中的顶点照明渲染
通常在一个Pass就可以完成。在这个Pass中,会逐顶点计算所有光源对该物体的照明,这是Unity中最快速的渲染路径,并具有广泛的硬件支持(游戏机上不支持)。因为顶点照明渲染路径仅仅是前向渲染路径的一个子集,因此在Unity5中作为一个遗留的渲染路径,未来可能会移除(Unity2021还在)。
-
可访问的内置变量和函数
一个顶点照明的Pass最多访问到8个逐顶点光源(如果小于8,其他的会被置为黑色)。顶点照明渲染路径中可以使用的内置变量:
名称 类型 描述 unity_LightColor half4[8] 光源颜色 unity_LightPosition float4[8] xyz分量是视角空间中的光源位置。如果光源是平行光,那么z分量值为0,其他光源类型z分量值为1 unity_LightAtten half4[8] 光源衰减因子。如果光源是聚光灯,x分量是cos(spotAngle/2),y分量是1/cos(spotAngle/4);如果是其他类型的光源,x分量是-1,y分量是1。z分量是衰减的平方,w分量是光源范围开根号的结果 unity_SpotDirection float4[8] 如果光源是聚光灯的话,值为视角方向的聚光灯的位置;如果是其他类型的光源,值为(0,0,1,0) 顶点照明渲染路径中可以使用的内置函数
函数名 描述 float3 ShadeVertexLights(float4 vertex, float3 normal) 输入模型空间中的顶点位置和法线,计算四个逐顶点光源的光照以及环境光。内部实现实际上调用了ShadeVertexLightsFull函数 float3 ShadeVertexLightsFull(float4 vertex, float3 normal, int lightCount, bool spotLight) 输入模型空间中的顶点位置和法线,计算lightCount个光源的光照以及环境光。如果spotLight值为true,那么这些光源会被当成聚光灯来处理,虽然结果更加准确,但计算更加耗时;否则按点光源处理。
延迟渲染路径
前向渲染的问题是,场景中有大量的实时光源时,性能会急速下降(每执行一个Pass都需要重新渲染一次物体)。
延迟渲染是一种更古老的渲染方法,由于前向渲染可能的瓶颈问题,近几年又流行起来。除了颜色缓冲和深度缓冲,延迟渲染会利用额外的缓冲区,统称为G缓冲(G-buffer),G是Geometry的缩写。G-buffer存储了我们所关心的表面(通常指的是离相机最近的表面)的其他信息,如表面法线、位置、用于光照计算的材质属性等。
-
延迟渲染的原理
主要包含了两个Pass:- 第一个Pass。不进行任何光照计算,仅通过深度缓冲技术,计算哪些片元是可见的。如果可见,就把它的相关信息存储到G缓冲。
- 第二个Pass。利用G缓冲的各个片元信息,例如表面法线、视角方向、漫反射系数等,进行真正的光照计算。
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
28Pass1{
// 第一个Pass不进行真正的光照计算
// 仅仅把光照计算需要的信息存储到G缓冲中
for(each primitive in this model){
for(each fragment covered by this primitive){
// 如果没有通过深度测试,说明该片元不可见
discard;
}else{
// 如果该片元可见,就把需要的信息存储到G缓冲中
writeGBuffer(materialInfo,pos,normal);
}
}
}
Pass2{
// 利用G缓冲中的信息进行真正的光照计算
for(each pixel in the screen){
if(the pixel is valid){
// 如果该像素的有效的,读取它对应的G缓冲中的信息
readGBuffer(pixel,materialInfo,pos,normal);
// 根据读取到的信息进行光照计算
float4 color=Shading(materialInfo,pos,normal,lightDir,viewDir);
// 更新帧缓冲
writeFrameBuffer(pixel,color);
}
}
}延迟渲染使用的Pass数目通常就是两个,和场景中包含的光源数目没有关系,只和我们使用的屏幕空间大小有关。原因在于需要的信息都存储在缓冲区中,缓冲区可以看成一张张2D图像,计算实际上就是在这些图像空间中进行的。
-
Unity中的延迟渲染
有两种,Unity5之前遗留的和Unity5.x中使用的,差别很小。遗留的不支持Unity5基于物理的StandardShader,以下仅讨论Unity5后使用的延迟渲染路径。延迟渲染路径适合在场景光源数目很多,如果使用前向渲染会造成性能瓶颈的情况下使用。延迟渲染路径中每个光源都可以按逐像素处理。但它有一些缺点:
- 不支持真正的抗锯齿(anti-aliasing)功能
- 不能处理半透明物体
- 对显卡有一定要求。显卡必须支持MRT(Multiple Render Targets)、ShaderMode3.0及以上、深度渲染纹理以及双面的模板缓冲
使用延迟渲染时,Unity要求我们提供两个Pass:
- 第一个用于渲染G缓冲。把物体的漫反射颜色、高光反射颜色、平滑度、法线、自发光、深度等信息渲染到屏幕空间的G缓冲中,每个物体这个Pass执行一次
- 第二个用于计算真正的光照模型。用上个Pass中渲染的数据来计算最终的光照颜色,在存储到帧缓冲中
默认的G缓冲区包含了以下几个渲染纹理(Render Texture, RT):
- RT0:格式是ARGB32,RGB通道存储漫反射颜色,A通道没有使用
- RT1:格式是ARGB32,RGB通道存储高光反射颜色,A通道存储高光反射指数部分
- RT2:格式是ARGB2101010,RGB通道存储法线,A通道没有使用
- RT3:格式是ARGB32(非HDR)或ARGBHalf(HDR),用于存储自发光+lightmap+反射探针(reflection probes)
- 深度缓冲和模板缓冲
第二个Pass中计算光照时,默认情况下仅可以使用Unity内置的Standard光照类型。如果要使用其他的光照模型,需要替换原有的Internal-DefferredShading.shader文件。
-
可访问的内置变量和函数
这些变量可以在UnityDefferredLibrary.cginc中找到它们的声明:延迟渲染路径中可以使用的内置变量
名称 类型 描述 _LightColor float4 光照颜色 _LightMatrix0(新版本是unity_WorldToLight) float4x4 从世界空间到光源空间的变换矩阵,可以用于采样cookie和光照衰减纹理 -
选择哪种渲染路径
参考官方文档,给出了4种渲染路径的详细比较(前向渲染、延迟渲染、遗留的延迟渲染、顶点照明):总体来说要根据游戏发布的目标平台来渲染渲染路径,如果当前显卡不支持所选的,会自动使用低一级的。
Unity的光源类型
Unity一共支持4种光源类型:平行光、点光源(point light)、聚光灯(spot light)和面光源(area light)。面光源仅在烘焙时才可发挥作用。
光源类型有什么影响
最常用的光源属性有5个:光源的位置、方向(到某点的方向)、颜色、强度以及衰减(到某点的衰减,与该点到光源的距离有关)。
-
平行光
作为太阳光,位置任意,几何属性只有方向,不会衰减。 -
点光源
照亮空间有限,由空间中的一个球体定义,表示由一个点发出的、向所有方向延申的光。有范围,有位置,会衰减。 -
聚光灯
照亮空间有限,由空间中一块锥形区域定义,表示由一个特定位置出发、向特定方向延申的光。有范围,有位置,会衰减。
以下内容引用自知乎文章
点光源和聚光灯都有个警告:Realtime and indirect bounce shadowing is not supported for Spot and Point lights。意思是不支持实时间接反弹阴影。查资料说是“这意味着由点光源创建的光线将继续穿过物体并在另一侧反射,除非被范围衰减。这可能会导致墙壁和地板发出“漏光”,因此灯必须小心放置以避免出现此类问题。然而,这在使用烘焙GI时不是问题。”如下将一块平面挡住另一块平面,聚光灯穿透上面的平面,继续照到下方了:
这个可能和光源模式也有关系,如果将聚光灯的Mode从Realtime改为Baked,就没有这个警告。如果将IndirectMultiplier间接系数改为0,也没有这个提示了,从字面意思看就是不使用这个光源进行间接反射阴影了。
Unity光源的Mode属性中Realtime/Mixed/Baked(实时/混合/烘焙),可能分别对应UE4中的Movable/Stationary/Static(可移动/固定/静态)。这里先不讨论这个问题了。
在前向渲染中处理不同的光源类型
如何使用前向渲染路径,在UnityShader中访问这3种光源的5个属性:位置、方向、颜色、强度、衰减。
-
实践
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
148Shader "UnityShadersLearn/Chapter9/ForwardRendering"
{
Properties
{
_Color ("Color Tint", Color) = (1.0, 1.0, 1.0, 1.0)
_Specular ("Specular Tint", Color) = (1.0, 1.0, 1.0, 1.0)
_Gloss ("Gloss", Range(8.0, 256)) = 20
}
SubShader
{
Pass
{
Tags {"LightMode"="ForwardBase"}
CGPROGRAM
// 保证在Shader中使用光照衰减等光照变量可以被正确赋值
#pragma multi_compile_fwdbase
#pragma vertex vert;
#pragma fragment frag;
#include "Lighting.cginc"
fixed4 _Color;
fixed4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
// 处理平行光的逐像素光照
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
// 计算环境光,只计算一次,所以放在ForwardBase,这里假设物体没有自发光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
// 使用_LightColor0得到最亮平行光的颜色和强度
// _LightColor0已经是颜色和强度相乘后的结果
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
// 平行光衰减为1
fixed atten = 1.0;
return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}
ENDCG
}
Pass
{
Tags {"LightMode"="ForwardAdd"}
// 计算的结果与缓冲区叠加
Blend One One
CGPROGRAM
// 保证在Shader中使用光照衰减等光照变量可以被正确赋值
#pragma multi_compile_fwdadd
#pragma vertex vert;
#pragma fragment frag;
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Color;
fixed4 _Specular;
float _Gloss;
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 worldNormal = normalize(i.worldNormal);
// 计算不同光源类型的方向
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
fixed3 diffuse = _LightColor0.rgb * _Color.rgb * saturate(dot(worldNormal, worldLightDir));
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 halfDir = normalize(worldLightDir + viewDir);
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(halfDir, worldNormal)), _Gloss);
// 计算不同光源类型的衰减
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif
return fixed4((diffuse + specular) * atten, 1.0);
}
ENDCG
}
}
FallBack "Specular"
}除了设置渲染路径外,还使用了#pragma指令。#pragma multi_compile_fwdbase保证在Shader中使用光照衰减等光照变量可以被正确赋值。
Base Pass处理了场景中最重要的平行光,如果有多个平行光,Unity只会选择最亮的平行光给Base Pass进行逐像素处理,其他平行光会按照逐顶点或在Additional Pass中按逐像素的方式处理。
如果场景中没有重要的平行光源,那么BasePass会被当做全黑的光源处理。
AdditionalPass处理其他逐像素光源。光照模式使用ForwardAdd,设置混合模式(不设置会覆盖原有的),然后要加上#pragma multi_compile_fwdadd(如果需要完整阴影可以用**_fullshadows版本)。在片元着色器中,用#ifdef/#else/#endif来判断光源类型,不同光源方向和衰减有区别。
内置矩阵_LightMatrix0,新版本Unity已经没有,请使用unity_WorldToLight。这个是从世界空间到光照空间的变换矩阵,用于将世界空间中的物体坐标,转到光照空间通过_LightTexture0采样获取衰减值。
-
实验:Base Pass和Additional Pass的调用
默认情况下光源的Render Mode是Auto,在此场景中一共5个光源,1个是平行光在BasePass中被处理,其余4个点光源在AdditionalPass中被处理,默认情况下一个物体可以接受除最亮的平行光以外的4个逐像素光照。开启帧调试器(Frame Debugger),Window->Analysis->FrameDebugger(Unity5是Widow->FrameDebugger)。可以看到前向渲染的绘制过程
当把光源的Render Mode设为Not Important时,这些光源就不会按逐像素光来处理
Unity处理这些点光源的顺序是按照重要度排序的,在这里因为所有点光源颜色和强度都相同,因此它们的重要度取决于它们距离胶囊体的远近。官方文档没有给出光源强度、颜色、距离物体的远近是如何影响重要度的,只知道有关系。
Unity的光照衰减
Unity使用一张纹理作为查找表来在片元着色器中计算逐像素光照的衰减,好处是不依赖数学公式,弊端如下:
- 需要预处理得到采样纹理,纹理大小也会影响衰减的精度
- 不直观也不方便,一旦存储到查找表,就无法使用其他数学公式来计算衰减
Unity默认还是用纹理查找的方式计算逐像素的点光源和聚光灯的衰减。
用于光照衰减的纹理
Unity内部使用一张名为_LightTexture0的纹理来计算光源衰减。(0,0)表示与光源位置重合的点的衰减值,(1,1)表示在光源空间中所关心的距离最远的点的衰减。
为了对_LightTexture0纹理采样得到指定点到该光源的衰减值,首先要得到该点到光源空间中的位置,通过_LightMatrix0变换矩阵得到:
1 | float3 lightCoord=mul(_LightMatrix0,float4(i.worldPosition,1)).xyz; |
然后可以使用这个坐标的模的平方对衰减纹理进行采样:
1 | //.rr是啥呢?首先是由点积得到光源的距离平方,这是一个标量,我们对这个变量进行.rr操作相当于构建了一个二维矢量,这个二维矢量每个分量的值都是这个标量值,由此得到一个二维采样坐标。这个操作是shader中常见的swizzling操作,Cg的官网上也有说明。 |
使用数学公式计算衰减
有时候希望可以在代码中利用公式来计算光源的衰减,如下:
1 | float distance=length(_WorldSpaceLightPos0.xyz-i.worldPosition.xyz); |
因为无法在Shader通过内置变量得到光源的范围、聚光灯的朝向、张开角度等信息,效果不尽人意。灵活度低。
Unity的阴影
阴影是如何实现的
实时渲染中,最常用的技术是Shadow Map。原理是它会首先把相机位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是那些相机看不到的地方。
在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity就会为该光源计算它的阴影映射纹理ShadowMap,本质是一张深度图。
Unity使用一个额外的LightMode标签被设置为ShadowCaster的Pass来更新光源的阴影映射纹理,这个Pass的渲染目标不是帧缓存,而是ShadowMap。如果没有,就会在Fallback指定的UnityShader中寻找。
在传统的阴影映射纹理实现中,会在正常渲染的Pass中把顶点位置变换到光源空间下,然后使用xy分量对ShadowMap进行采样得到深度值,判断是否处于阴影中。
Unity5中,使用了屏幕空间的阴影映射技术(Screenspace Shadow Map)。首先通过调用LightMode为ShadowCaster的Pass得到ShadowMap和相机深度纹理;然后据此得到屏幕空间的阴影图;如果相机的深度图中记录的表面深度大于转换到ShadowMap中的深度值,说明表面可见但在阴影中。如果想要一个物体接受来自其他物体的阴影,只需要在Shader中对阴影图采样。
总结,一个物体接受阴影,以及向其他物体投射阴影是两个过程:
- 接受阴影。在Shader中对ShadowMap进行采样,把采样结果和最后的光照结果相乘
- 投射阴影。必须把该物体加入到光源的ShadowMap计算中。
不透明物体的阴影
-
让物体投射阴影
在Mesh Renderer组件打开Cast Shadows和Receive Shadows属性,光源处也需要开启投射阴影直接使用上一小节中的ForwardRendering,尽管只有BasePass和AdditionalPass,没有LightMode为ShadowCaster的Pass,也能正常投射阴影,这是因为Fallback起了作用,Fallback使用了Specular,而Specular的Fallback调用了VertexLit,这里进行了渲染ShadowMap和深度图的工作。
关键的内置着色器代码(DefaultResourcesExtra\Normal-VertexLit.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// Pass to render object as a shadow caster
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
}右边的平面默认是不会从背面朝正面投影的,需要将CastShadows从On设置为TwoSided。
-
让物体接收阴影
先在BasePass中包含一个新的内置文件:1
#include "AutoLight.cginc" // 下面计算阴影时所用的宏是在这个文件中声明的
在顶点着色器的输出结构体v2f中添加内置宏SHADOW_COORDS:
1
2
3
4
5
6
7
8
9struct v2f{
float4 pos:SV_POSITION;
float3 worldNormal:TEXCOORD0;
float3 worldPos:TEXCOORD1;
// 声明一个用于对阴影纹理采样的坐标
// 参数是下一个可用的插值寄存器的索引值,2
SHADOW_COORDS(2)
};在顶点着色器返回之前添加内置宏TRANSFER_SHADOW:
1
2
3
4
5
6
7
8
9v2f vert(a2v v{
v2f o;
...
// Pass shadow coordinates to pixel shader
// 用于在顶点着色器中计算上一步中声明的阴影纹理坐标
TRANSFER_SHADOW(o);
return o;
})在片元着色器中计算阴影,使用了内置宏SHADOW_ATTENUATION:
1
2// Use shadow coordinates to sample shadow map
fixed shadow=SHADOW_ATTENUATION(i);最后在片元着色器中,把阴影值shadow和漫反射以及高光反射颜色相乘:
1
return fixed4(ambient+(diffuse+specular)*atten*shadow, 1.0);
SHADOW_COORDS、TRANSFER_SHADOW、SHADOW_ATTENUATION是计算阴影的“三剑客”,可以在AutoLight.cginc中找到他们的声明
使用帧调试器查看阴影绘制过程
打开FrameDebug,渲染事件可以分为4个部分:
- UpdateDepthTexture:更新相机深度纹理
- RenderShadowmap:渲染得到的平行光阴影映射纹理
- CollectShadows:根据深度纹理和阴影映射纹理得到屏幕空间的阴影图
- 最后绘制渲染结果
使用帧调试器查看阴影绘制过程
正方体对深度纹理的更新结果
屏幕空间的阴影图
Unity绘制屏幕阴影的过程
统一管理光照衰减和纹理
实际上,光照衰减和阴影对物体最终渲染结果的影响本质上是相同的——我们都是把光照衰减和阴影值及光照结果相乘得到最终的渲染结果。通过内置的UNITY_LIGHT_ATTENUATION,可以同时计算两个信息。
1 | // UNITY_LIGHT_ATTENUATION not only compute attenuation, but also shadow infos |
透明度物体的阴影
- 对大多数不透明物体来说,把Fallback设为VertexLit就可以得到正确的阴影。对于透明物体,要小心处理。
- 透明度测试如果使用VertexLit、Diffuse、Specular等作为回调,往往无法得到正确的阴影(镂空处的阴影显示不出来)。
- 可以使用Transparent/Cutout/VertexLit作为Fallback,在这个Fallback中,使用了名为_Cutoff的属性来进行透明度测试。。
- 可以使用上节让物体接收阴影的方法同样可以让透明度测试的物体接收阴影
- 需要MeshRenderer组件中的CastShadows属性设置为TwoSided,才能得到更为真实的阴影(背面的镂空遮挡)



- 对于透明度混合,因为关闭了深度写入,所以阴影处理很复杂。所有内置的透明度混合的UnityShader,如Transparent/VertexLit都不包含阴影投射的Pass,它不会向其他物体投射阴影,也不会接收来自其他物体的阴影。
- 如果强行设置它们的Fallback为VertexLit、Diffuse等,阴影会投射到半透明物体表面,也是不正确的(阴影没有穿过透明物体)。
本书使用的标准UnityShader
之前作者一直强调,代码不能用到实际项目中,因为缺少了一些光照计算。现在已经学习了Unity中所有的基础光照计算,如多光源、阴影和光照衰减,可以整合到一起实现一个标准光照着色器了。
在本书资源的Assets/Shaders/Common中提供了BumpedDiffuse和BumpedSpecular,都包含了对法线纹理、多光源、光照衰减和阴影的处理。BumpedDiffuse使用了Phong光照模型,BumpedSpecular使用了Blinn-Phong光照模型。
1 | // Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld' |
查看内置宏时的一些参考链接