Unity Shadow 阴影(翻译七)
当一个物体位于光源和另一个物体之间时,它可能会阻止部分或全部光线到达另一个物体。这些光线照亮了第一个物体就不再可能照亮第二个物体。因此,第二个物体有一部不发光,而不发光的区域位于第一个物体的阴影下。我们通常是这样描述:第一个物体投射了一个阴影到第二个物体。
本篇摘要:
- 探索Unity中的阴影渲染
- 投射一个方向光阴影
- 接收一个方向光阴影
- 支持对聚光源和点光源阴影
方向光阴影-Direction
前面写的光照shader产生了相当真实的效果,可它假设着来自每个光源的光线最终都会击中它的片元,但是这只有在那些光线没有被遮挡才成立。
当一个物体位于光源和另一个物体之间时,它可能会阻止部分或全部光线到达另一个物体。这些光线照亮了第一个物体就不再可能照亮第二个物体。因此,第二个物体有一部不发光,而不发光的区域位于第一个物体的阴影下。我们通常是这样描述:第一个物体投射了一个阴影到第二个物体。
实际上,在全光照和全阴影的存在过渡区,被称为半阴影。这是因为所有光源都有一个体积,因此,这些区域只有部分光源是可见的,意味着它是部分阴影。光源远大,表面距离阴影投射器越远,半影区域也就越大。
但是Unity不支持半影,只支持软阴影soft shadow,但它是阴影过滤算法。
启用阴影-Enable Shadow
先关闭环境光,这样会更容易看见阴影。
没有阴影,物体间的空间视觉感不太强。在QualitySetting可以打开或关闭阴影。
同时确保光源开启投射阴影,分辨率依赖于上面的quality设置
阴影贴图-Shadow map
Unity是如何把阴影添加到屏幕?上面所有物体使用的standard着色器,有一些方法确定光线是否被阻挡。
要搞清楚一个点是否在阴影中,可以通过在场景中从光线到表面片元投射光线,如果光线在到达表面之前击中某些东西,说明它就被阻挡了。这些事是物理引擎做的,但是要计算每个片元与每个光是不实际的,而且还要把结果传递给GPU。
现在有许多支持实时阴影的技术,它们各有优劣。而Unity采用了最常用的技术:Shadow Mapping。这意味着unity把阴影数据存储至纹理中。现在来看看它是如何工作的。
打开frame Debugger,Window/Frame Debugger。点击Enable,按顺序查看面板信息。注意看看每帧在gameScene视图中的不同,以及阴影的开启。
当启用阴影绘制时,这个绘制过程变得非常复杂:有更多的渲染阶段,和更多的draw call。阴影绘制非常昂贵!
渲染深度纹理-Rendering to the Depth Texture
当方向阴影激活后,Unity在渲染过程开启一个depth pass通道计算。结果存储在与屏幕分辨率相匹配的纹理,这个pass通道会渲染整个屏幕,但是只收集每个片元的深度信息。这些信息与GPU用于确定一个片段渲染结束时在先前渲染的片段之上(前)还是之下(后)的信息相同。
这个数据对应在裁剪空间(clip space)坐标的z分量值。而裁剪空间是定义摄像机能看见的区域,深度信息最终存储为0-1范围内的值。在debugger查看该纹理时,近裁切面附近的纹理显示趋近为(白)浅色,远裁切面附近的纹素texel,颜色趋近黑(暗)色。
这些信息实际上与阴影没有太多直接关系,但Unity在后面的pass通道使用了它。
渲染阴影贴图-Rendering to Shadow Maps
这步主要工作:先渲染第一个光源的阴影贴图,然后就会渲染第二个光源的阴影贴图。
再一次渲染整个屏幕,并再次把深度信息存储在纹理中。但是,这此的屏幕渲染是从光源位置角度进行的,实际上是把光源作为摄像机。这意味着用深度值告诉了我们光线击中物体之前走了多远距离,这可以用来确定什么东西被遮蔽了!
阴影贴图记录了实际的几何图形的深度信息。而法线贴图是为了添加粗糙表面的一种错觉, 阴影贴图会忽略它们。因此,阴影不受法线贴图的影响。
由于我们使用方向光,这些光模拟的摄像机是正交投影,没有透视投影。因此它们模拟的相机的位置精确性就不那么重要。Unity将定位常规相机使其能够看见视野内所有物体。
事实上,原来Unity渲染整个场景不是每个光只渲染一次,而是每个光要渲染四次!
这个阴影纹理被分成四个象限,每个象限从不同的角度呈现。这是因为我们选择使用 Four Cascades(QualitySetting)。如果我们设置为Two Cascades,就是每个光渲染两次; 如果设置没有,只会渲染一次。我们接下来要探查阴影质量与该项设置的关系。Unity为什么渲染这么 多次。
收集阴影-Collecting Shadows
我们已经从摄像机的角度得到场景的深度信息,也有了从每个光模拟的相机视角得到的深度信息,这些数据存在不同的裁剪空间。但是我们知道这些空间的相对位置和方向,因此能从一个空间转换到另一个空间。这允许我们从两个视角比较深度测量。理论上讲,我们有两个向量应该在同一点交会结束,这样相机和光源都能看见该点,说明它被点亮了。如果光的向量在到达该点之前结束,则光被挡住,这意味着该点被阴影化。
当摄像机看不到一个点时? 看不到的这些点被隐藏在距离相机更近的其他点后面。 场景深度纹理仅包含最接近的点。 因此没有时间浪费在评估隐藏点。
Unity通过渲染一个单独的覆盖整个视野的面片来创建这些纹理,它使用了Hidden/Internal-ScreenSpaceShadows shader的通道,每个片元从场景和光源的深度纹理采样,进行比较,渲染最终阴影值到屏幕空间的阴影纹理。亮的纹素值设为1,阴影纹素值设为0。此时Unity能执行过滤,创建柔和的阴影。
为什么Unity在渲染和收集间交替?
每个光需要它自己的屏幕空间阴影贴图,然而从光源位置视野渲染的阴影贴图能被重复使用。
采样阴影贴图-Sampling the Shadow Maps
最后,Unity完成了阴影渲染。现在屏幕是常规渲染,只有一个更改:光照颜色与它的阴影贴图的值相乘。这就消除了被遮挡的光线。渲染的每个片元都要采样阴影贴图,每个最终隐藏在其他对象之后的片元会最后绘制。因此这些片元最后能接收到最终能遮挡它们的对象的阴影。当在frame debugger步进调试观察时,您还可以看到阴影在实际投射它的对象之前出现。当然这些错误只在渲染帧时很明显,一旦完成渲染就是正确的了
阴影质量-Shadow Quality
虽然场景是从光源的方向进行渲染,但是该方向与场景内摄像机视野方向不匹配。因此阴影贴图的纹素与最终呈现图像的纹素是没有对齐的,会出现锯齿。阴影贴图的分辨率也会不同,最终图像的分辨率是由显示设置决定的,而阴影贴图的分辨率由阴影质量设置决定。
当阴影贴图的纹素最后渲染的比最终图像大时,将很明显:阴影的边缘出现叠加,在使用硬阴影时非常明显。
在质量设置面板修改使用hard shadow、lowest resolution、no cascades。就会看见满屏的锯齿。
“阴影是一张纹理”
现在就非常明显了。但是上图有些阴影出现在了不该出现的地方。
距离摄像机越近的阴影,它们的纹素变得越大。这是由于阴影贴图当前覆盖了场景相机的整个可视区域。在QualitySetting面板通过降低阴影覆盖的区域,来提升靠近相机区域的阴影质量。
通过限制靠近屏幕相机的阴影区域,我们能使用相同的阴影纹理去覆盖更多小区域。结果是能得到更好的阴影。但是会丢失更远区域的阴影细节,因为当阴影接近最大距离时会逐渐消失。
理想情况是,既要获得近距离高质量阴影,同时也要保留远处的阴影。因为远处的阴影最后渲染在较小的屏幕区域,就可以用作低分辨率阴影纹理。这就是Shadow Cascades的工作。当启用该选项,多个阴影贴图渲染进同一张纹理,每张贴图对应某些距离来使用。
当使用FourCascades,上图结果看起来比之前的要好,尽管我们使用了同一张纹理分辨率,我们更有效的使用了纹理。不过缺点就是我们现在至少要渲染场景3次以上。当渲染屏幕空间阴影纹理时,Unity关注从正确Cascade采样,如下图CascadeSplits:一个cascade结束是下一个的开始。
可以控制cascade的范围,作为阴影距离的一部分。也能通过改变_Shading Mode/Miscellaneous/Shadow Cascades_观察scene视图的变化。
上图显示的cascade形状(覆盖区域)是可以通过_Shadow Projection_调整,默认是_Stable Fit_:这个模式cascade条带选择的区域基于距离摄像机位置的远近。其他模式是_Close Fit_:使用相机的深度信息替代,在相机可视方向产生一条规则的条带。
Close Fit模式可以更高效的利用阴影纹理,绘制更高质量的阴影。然而,该阴影投射模式(ShadowProjection)取决于阴影产生后位置和方向以及相机参数。结果是,当移动或旋转相机,阴影贴图也会跟着移动。这就是著名的阴影抖动。所以Stale Fit是引擎默认的选项。
Stable Fit模式下,在相机位置改变时Unity能够对齐纹理,纹素看起来好像没动。实际上cascade移动了,只是在cascade相互过渡时阴影会发生改变。如果没有注意到cascade改变,就不容易察觉到。
阴影“痤疮”(0!什么鬼)
当我们使用低质量的硬阴影时,我们看见一些阴影出现在不正确的地方。不幸的是,不管如何设置_Quality Setting_都会发生。
Shadow Acne
阴影贴图中每个纹素表示光线击中表面的点。然而,这些纹素不是单独点。它们最后要覆盖很大的区域并且与光的方向对 齐,而不是与表面一致。结果时,它们会像黑色瓦片最终黏在、穿过、伸出表面;当阴影纹理的一部分从投射出阴影的表面 伸出时,表面看起来也会产生阴影。
阴影凸起的另一个来源是数字精度的限制,当使用非常小的距离时这些限制会导致不正确的结果。默认是0.05.
避免该问题的一个方法是:当渲染阴影纹理时增加深度偏移。这个_偏差系数_目的是增加‘光投射到表面距离’,把阴影‘推进’表面内。
较低的Bias系数会产生粉刺,而较高的偏差系数就会有另一个问题:当投射阴影的对象逐渐远离光源时,阴影也会逐渐飘离原对象。使用较小的值问题还可接受,但太大的值会导致物体与该物体的阴影不再相连接了,好像飞起来了。
除了距离bias偏差,还有法线偏差。该系数辅助调整阴影投射:沿着法线,将投射的阴影顶点向内‘推’。该值也会改善“阴影粉刺”,但是越大的值越会使阴影变得更小并且有可能使阴影中间出现洞。
best bias settings?没有最优的默认值,必须不停的实验调整 。
抗锯齿
Anti-Aliasing:图形边缘锯齿缓和。在Unity开启了4倍抗锯齿,感觉并没有达到想要的抗锯齿效果。
Unity采用的多重采样抗锯齿方案:MSAA,通过沿三角形边缘执行超级采样以消除边缘锯齿,更重要的是Unity渲染屏幕空间阴影时,它使用了一个单独四方面片覆盖整个可视区域。结果是,这就没有了三角形边缘,因此MSAA对屏幕空间阴影纹理采样就没有效果了。MSAA对最终图像有效,但阴影值是取之屏幕空间阴影纹理,当亮表面紧挨着暗表面被阴影覆盖时就非常明显。明暗之间的边缘是反锯齿的,而阴影边缘则不是。
当然也有FXAA,是屏幕后处理抗锯齿,效果挺好!
投射阴影
通过上面我们知道了Unity如何创建方向光阴影,是时候写自己的Shader来支持阴影了。当前光照shader既不支持投射阴影也不支持接收阴影。
首先来处理投射阴影:我们知道对于方向光阴影Unity会渲染多次屏幕。对每个阴影纹理一次是深度pass渲染,一次是每个光源渲染。而屏幕空间阴影纹理是屏幕效果暂时与我们无关。阴影渲染Pass标签是_ShadowCaster。_因为我们只对深度值感兴趣,它与别的Pass相比应该会简单。增加一个pass
1
2
3
4
5
6
7
8
9
Pass{
Tags{"LightMode" = "ShadowCaster"}
CGPROGRAM
#pragma target 3.0
#pragma vertex MyVertexProgram
#pragma fragment MyFragmentProgram
#include "MyShadow.cginc"
ENDCG
}
创建一个_MyShadow.cginc_文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#if !defined(MY_SHADOW_INCLUDE)
#define MY_SHADOW_INCLUDE
#include “UnityCG.cginc”
struct InputData {
float4 position : POSITION;
};
float4 MyVertexProgram(InputData i) : SV_POSITION{
return UnityObjectToClipPos(i.position);
}
half4 MyFragmentProgram() : SV_TARGET{
return 0;
}
#endif
上面写完就嫩产生方向光阴影了。下面开始用代码调优阴影质量。
偏差-Bias
我们要支持阴影的偏移。在渲染深度Pass时该值是0,但当渲染阴影纹理时,偏差值取光照组件设置。我们要做的就是:在顶点函数中在裁切空间下,对顶点坐标应用深度偏差。UnityCG函数_UnityApplyLinerShadowBias:_
1
2
3
4
float4 MyVertexProgram(InputData i) : SV_POSITION{
float4 position = UnityObjectToClipPos(i.position);
return UnityApplyLinearShadowBias(position);
}
在裁剪空间增加Z分量,复杂的是在其次坐标空间下,必须补偿透视投影,这样偏移不会随着与相机距离改变而改变,也必须确保结果不会越界。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
float4 UnityApplyLinearShadowBias(float4 clipPos)
{
#if defined(UNITY_REVERSED_Z)
// We use max/min instead of clamp to ensure proper handling of the rare case
// where both numerator and denominator are zero and the fraction becomes NaN.
clipPos.z += max(-1, min(unity_LightShadowBias.x / clipPos.w, 0));
float clamped = min(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE);
#else
clipPos.z += saturate(unity_LightShadowBias.x / clipPos.w);
float clamped = max(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE);
#endif
clipPos.z = lerp(clipPos.z, clamped, unity_LightShadowBias.y);
return clipPos;
}
同时支持Normal Bias,必须根据法向量移动顶点坐标。因此,添加一个normal变量。然后可以使用UnityCG定义的UnityClipSpaceShadowCasterPos函数
1
2
3
4
5
float4 MyVertexProgram(InputData i) : SV_POSITION{
//float4 position = UnityObjectToClipPos(i.position);
float4 position = UnityClipSpaceShadowCasterPos(i.position, i.normal);
return UnityApplyLinearShadowBias(position);
}
先将顶点坐标转换到世界空间,然后转换到裁剪空间。计算光的方向,计算法线和光的角度,取正弦值,最后转与观察投影矩阵相乘转到裁剪空间。
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
float4 UnityClipSpaceShadowCasterPos(float4 vertex, float3 normal)
{
float4 wPos = mul(unity_ObjectToWorld, vertex);
if (unity_LightShadowBias.z != 0.0)
{
float3 wNormal = UnityObjectToWorldNormal(normal);
float3 wLight = normalize(UnityWorldSpaceLightDir(wPos.xyz));
// apply normal offset bias (inset position along the normal)
// bias needs to be scaled by sine between normal and light direction
// (http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/)
//
// unity_LightShadowBias.z contains user-specified normal offset amount
// scaled by world space texel size.
float shadowCos = dot(wNormal, wLight);
float shadowSine = sqrt(1-shadowCos*shadowCos);
float normalBias = unity_LightShadowBias.z * shadowSine;
wPos.xyz -= wNormal * normalBias;
}
return mul(UNITY_MATRIX_VP, wPos);
}
写完就具备了完全的阴影投射
接收阴影
First,我们先关注主方向光的阴影,因为该光源属于BasePass,必须要先适配。当主方向光投射阴影,Unity会找一个启用了SHADOWS_SCREEN关键字的shader变体。所以我们要在Base Pass创建两个变体,同之前使用顶点光关键字类似:一个无,一个是该关键字。
1
2
#pragma multi_compile _ VERTEXLIGHT_ON
#pragma multi_compile _ SHADOWS_SCREEN
该basePass有两个multi_compile指令,每个都是单关键字。因此编译后这里会有4个变体:
1
2
3
4
5
6
7
8
9
10
11
// Total snippets: 3
// --
// Snippet #0 platforms ffffffff:
SHADOWS_SCREEN VERTEXLIGHT_ON
4 keyword variants used in scene:
<no keywords defined>
SHADOWS_SCREEN
VERTEXLIGHT_ON
SHADOWS_SCREEN VERTEXLIGHT_ON
(老版本Unity有可能出现)当增加了multi_compile指令后,shader编译器会提示关于__ShadowCoord_不存在。这是因为_UNITY_LIGHT_ATTENUATION_宏在使用阴影时的行为不同导致。在MyLighting_shadow.cginc顶点函数快速修复
1
2
3
4
#if defined(UNITY_SCREEN) float attenuation = 1;
#else
UNITY_LIGHT_ATTENUATION(attenuation, 0, i.worldPos);
#endif
采样阴影
Secend,采样屏幕空间阴影纹理。
Third,需要获取屏幕空间纹理坐标,从顶点函数传递给片元函数。在插值器Interpolator添加一个float4 变量以支持传递阴影纹理坐标。从裁剪空间开始(裁剪空间顶点坐标)。
1
2
3
4
5
6
7
8
9
10
11
12
struct Interpolator{
#if defined(SHADOWS_SCREEN)
float4 shadowCoordinate : TEXCOORD6;
#endif
}
Interpolators MyVertexProgram(VertexData v) {
//。。。
#if defined(SHADOWS_SCREE)
i.shadowCoordinate = i.position;
#endif
//。。。
}
AutoLignt.cginc定义了Sampler2D _ShadowMapTexture,可以通过它访问屏幕阴影纹理。但是要覆盖整个屏幕,就需要屏幕空间坐标。在裁剪空间,XY坐标范围是[-1, 1],而屏幕空间下是[0,1];然后偏移坐标与屏幕左小脚等于0对齐。因为我们处理的使透视变换,偏移坐标值取决于距离,这里的偏移值等于加上齐次坐标的w分量之后的一半。
1
2
3
4
#if defined(SHADOWS_SCREEN)
i.shadowCoordinate.xy = (i.position.xy + i.position.w) * 0.5;
i.shadowCoordinate.zw = i.position.zw;
#endif
上图的投影错误,还需要通过x和y除以齐次坐标进一步转换
上图结果仍然是错误的,影子被拉伸了。这是由于在顶点函数计算导致,不应该在传递给片元函数时提前修改原始数据,需要保持它们的独立性。在片元函数再次除以w.
此时,影子是上下颠倒的。如果它们被翻转,这意味着你的图形Direct3D屏幕空间Y坐标从0向下到1,而不是向上。要与此同步,翻转顶点的Y坐标。
1
2
3
4
#if defined(SHADOWS_SCREEN)
i.shadowCoordinate.xy = (float2(i.pos.x, -i.pos.y) + i.pos.w);// (i.pos.xy + i.pos.w) * 0.5;
i.shadowCoordinate.zw = i.pos.zw;
#endif
内置函数使用
SHADOW_COORDS宏定义纹理坐标
TRANSFRE_SHADOW宏获取阴影纹理坐标(转换)
1
#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
SHADOW_ATTENUATION宏阴影纹理明暗衰减
1
2
3
#define SHADOW_COORDS
(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
UNITY_LIGHT_ATTENUATION宏包含了SHADOW_ATTENUATION宏使用,可替换之
当启用SHADOWS_SCREEN指令时,会自动计算,不启用不计算,没有任何损失。
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
struct Interpolators {
//...
// #if defined(SHADOWS_SCREEN)
// float4 shadowCoordinates : TEXCOORD5;
// #endif
SHADOW_COORDS(5)
//...
};
Interpolators MyVertexProgram (VertexData v) {
//...
// #if defined(SHADOWS_SCREEN)
// i.shadowCoordinates = i.position;
// #endif
TRANSFER_SHADOW(i);
//...
}
UnityLight CreateLight (Interpolators i) {
//...
#if defined(SHADOWS_SCREEN)
float attenuation = SHADOW_ATTENUATION(i);
#else
UNITY_LIGHT_ATTENUATION(attenuation, 0, i.worldPos);
#endif
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
//...
}
ComputeScreenPos函数
1
2
3
4
5
6
7
8
9
10
11
12
13
inline float4 ComputeNonStereoScreenPos(float4 pos) {
float4 o = pos * 0.5f;
o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
o.zw = pos.zw;
return o;
}
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;
}
聚光灯阴影
关闭方向光,增加聚光灯后,竟然直接有阴影了。这是Unity宏带来的便利。
再看帧调试器
上图对于聚光灯源阴影的渲染工作量很少
不同之处:
- 没有方向光独立的深度pass和屏幕空间阴影pass,而是直接渲染阴影纹理;
- 与方向光渲染阴影还有很大的差别之处:聚光灯光线不是平行的,因此用光的位置模拟相机视角会得到一个透视视角,结果就是不支持阴影分段渲染(cascades);
- normal bias(法线偏差)只支持方向光阴影,对于其他光源类型简单的置为0;
- 采样代码不同。
相同之处:
- 投射阴影的这段代码通用。
采样阴影纹理
由于聚光灯不使用屏幕空间的阴影,这段采样纹理代码就有点不一样。因此,如果我们想要使用软阴影,我们必须在fragment程序中进行过滤。而Unity宏已经做了过滤计算UnitySampleShadowmap。
1
2
3
4
5
6
7
8
9
//阴影坐标把顶点坐标从模型空间转到世界空间再转到光的阴影空间得到。
// - Spot light shadows
#if defined (SHADOWS_DEPTH) && defined (SPOT)
#define SHADOW_COORDS(idx1) unityShadowCoord4 _ShadowCoord : TEXCOORD##idx1;
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul(unity_WorldToShadow[0], mul(unity_ObjectToWorld,v.vertex));
#define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord)
#endif
然后SHADOW_ATTENUATION宏使用UnitySampleShadowmap函数采样阴影映射。这个函数定义在_UnityShadowLibrary_,_AutoLight_文件引用了它。当使用硬阴影时,该函数对阴影纹理采样一次。当使用软阴影时,它对纹理采样四次并对结果取平均值。这个结果没有用于屏幕空间阴影的过滤效果好,但是速度快得多。
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
// Spot light shadows
inline fixed
UnitySampleShadowmap (float4 shadowCoord)
{
// DX11 feature level 9.x shader compiler (d3dcompiler_47 at least)
// has a bug where trying to do more than one shadowmap sample fails compilation
// with "inconsistent sampler usage". Until that is fixed, just never compile
// multi-tap shadow variant on d3d11_9x.
#if defined (SHADOWS_SOFT) && !defined (SHADER_API_D3D11_9X)
// 4-tap shadows
#if defined (SHADOWS_NATIVE)
#if defined (SHADER_API_D3D9)
// HLSL for D3D9, when modifying the shadow UV coordinate, really wants to do
// some funky swizzles, assuming that Z coordinate is unused in texture sampling.
// So force it to do projective texture reads here, with .w being one.
float4 coord = shadowCoord / shadowCoord.w;
half4 shadows;
shadows.x = UNITY_SAMPLE_SHADOW_PROJ(_ShadowMapTexture, coord + _ShadowOffsets[0]);
shadows.y = UNITY_SAMPLE_SHADOW_PROJ(_ShadowMapTexture, coord + _ShadowOffsets[1]);
shadows.z = UNITY_SAMPLE_SHADOW_PROJ(_ShadowMapTexture, coord + _ShadowOffsets[2]);
shadows.w = UNITY_SAMPLE_SHADOW_PROJ(_ShadowMapTexture, coord + _ShadowOffsets[3]);
shadows = _LightShadowData.rrrr + shadows * (1-_LightShadowData.rrrr);
#else
// On other platforms, no need to do projective texture reads.
float3 coord = shadowCoord.xyz / shadowCoord.w;
half4 shadows;
shadows.x = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[0]);
shadows.y = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[1]);
shadows.z = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[2]);
shadows.w = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[3]);
shadows = _LightShadowData.rrrr + shadows * (1-_LightShadowData.rrrr);
#endif
#else
float3 coord = shadowCoord.xyz / shadowCoord.w;
float4 shadowVals;
shadowVals.x = SAMPLE_DEPTH_TEXTURE (_ShadowMapTexture, coord + _ShadowOffsets[0].xy);
shadowVals.y = SAMPLE_DEPTH_TEXTURE (_ShadowMapTexture, coord + _ShadowOffsets[1].xy);
shadowVals.z = SAMPLE_DEPTH_TEXTURE (_ShadowMapTexture, coord + _ShadowOffsets[2].xy);
shadowVals.w = SAMPLE_DEPTH_TEXTURE (_ShadowMapTexture, coord + _ShadowOffsets[3].xy);
half4 shadows = (shadowVals < coord.zzzz) ? _LightShadowData.rrrr : 1.0f;
#endif
// average-4 PCF
half shadow = dot (shadows, 0.25f);
#else
// 1-tap shadows
#if defined (SHADOWS_NATIVE)
half shadow = UNITY_SAMPLE_SHADOW_PROJ(_ShadowMapTexture, shadowCoord);
shadow = _LightShadowData.r + shadow * (1-_LightShadowData.r);
#else
half shadow = SAMPLE_DEPTH_TEXTURE_PROJ(_ShadowMapTexture, UNITY_PROJ_COORD(shadowCoord)) < (shadowCoord.z / shadowCoord.w) ? _LightShadowData.r : 1.0;
#endif
#endif
return shadow;
}
点光源阴影
如果直接使用点光源,会有编译报错:undeclared identifier ‘UnityDecodeCubeShadowDepth’。该函数在_UnityCG.cginc_文件。
所以根据引用结构,需要把UnityPBSLighing文件放在第一位引用。就不会报错了。
投射阴影
从帧调试器查看,左边一个光要渲染6次,两盏光就是12次了。有很多个RenderJobPoint渲染了。结果是,点光源的阴影纹理是一个立方体贴图,而立方体贴图是通过相机在6个不同方向观察场景,每个方向渲染一面组成六面体,前面1.4讲过把光源模拟相机对屏幕渲染。所以点光源阴影计算很费,尤其是实时点光源阴影。
当渲染点光源阴影纹理时,Unity引擎会找shader变体关键字SHADOWS_CUBE,而SHADOWS_DEPTH关键字只适用于方向光和聚光灯。为了支持点光源阴影,Unity提供了一个特殊编译指令
1
2
3
4
5
6
7
8
9
#pragma multi_compile_shadowcaster
// --
// Snippet #2 platforms ffffffff:
SHADOWS_CUBE SHADOWS_DEPTH
2 keyword variants used in scene:
SHADOWS_DEPTH
SHADOWS_CUBE
所以,需要创建一个独立的处理程序。这里首先要计算光到表面的距离,但得知道光到表面的方向。在顶点函数先转换顶点坐标所在世界空间,再计算光的方向。然后在片元函数计算该方向向量长度再与bias偏差相加。然后再除以点光源的范围映射到[0.1]再与长度相乘,最后解码。而_LightPositionRange.w = 1/range已经计算好了隐射范围,直接用。
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
#if defined(SHADOWS_CUBE)
struct Interplotars {
float4 position : SV_POSITION;
float3 lightVec : TEXCOORD0;
};
Interplotars MyVertexProgram(InputData v){
Interplotars i;
i.position = UnityObjectToClipPos(v.position);
i.lightVec = mul(unity_ObjectToWorld, v.position).xyz - _LightPositionRange.xyz;
//float4 position = UnityClipSpaceShadowCasterPos(i.position, i.normal);//方向光源:简单的裁剪空间顶点坐标
return i;
}
half4 MyFragmentProgram(Interplotars i) : SV_TARGET{
float depth = length(i.lightVec) + unity_LightShadowBias.x;
depth *= _LightPositionRange.w;
return UnityEncodeCubeShadowDepth(depth);
}
#else
float4 MyVertexProgram(InputData i) : SV_POSITION{
//float4 position = UnityObjectToClipPos(i.position);
float4 position = UnityClipSpaceShadowCasterPos(i.position, i.normal);
return UnityApplyLinearShadowBias(position);
}
half4 MyFragmentProgram() : SV_TARGET{
return 0;
}
#endif
UnityEncodeCubeShadowDepth函数:
1
2
3
4
5
6
7
8
9
// Shadow caster pass helpers
float4 UnityEncodeCubeShadowDepth (float z)
{
#ifdef UNITY_USE_RGBA_FOR_POINT_SHADOWS
return EncodeFloatRGBA (min(z, 0.999));
#else
return z;
#endif
}
1
2
3
4
5
6
7
8
9
10
// 使用浮点类型cube——map,存储再8位RGBA纹理
inline float4 EncodeFloatRGBA( float v )
{
float4 kEncodeMul = float4(1.0, 255.0, 65025.0, 16581375.0);
float kEncodeBit = 1.0/255.0;
float4 enc = kEncodeMul * v;
enc = frac (enc);//返回小数部分
enc -= enc.yzww * kEncodeBit;
return enc;
}
采样阴影纹理
在additional pass的编译指令,Unity宏已经做了。
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
//同样计算光的方向,然后采样cubeMap。区别是float3类型而不是float4,不需要齐次坐标。
// - Point light shadows
#if defined (SHADOWS_CUBE)
#define SHADOW_COORDS(idx1) unityShadowCoord3 _ShadowCoord : TEXCOORD##idx1;
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz;
#define SHADOW_ATTENUATION(a) UnitySampleShadowmap(a._ShadowCoord)
#endif
//
// Point light shadows
//在这种情况下,UnitySampleShadowmap采样一个立方体地图,而不是2D纹理。
#if defined (SHADOWS_CUBE)
samplerCUBE_float _ShadowMapTexture;
inline float SampleCubeDistance (float3 vec)
{
#ifdef UNITY_FAST_COHERENT_DYNAMIC_BRANCHING
return UnityDecodeCubeShadowDepth(texCUBElod(_ShadowMapTexture, float4(vec, 0)));
#else
return UnityDecodeCubeShadowDepth(texCUBE(_ShadowMapTexture, vec));
#endif
}
inline half UnitySampleShadowmap (float3 vec)
{
float mydist = length(vec) * _LightPositionRange.w;
mydist *= 0.97; // bias
#if defined (SHADOWS_SOFT)
float z = 1.0/128.0;
float4 shadowVals;
shadowVals.x = SampleCubeDistance (vec+float3( z, z, z));
shadowVals.y = SampleCubeDistance (vec+float3(-z,-z, z));
shadowVals.z = SampleCubeDistance (vec+float3(-z, z,-z));
shadowVals.w = SampleCubeDistance (vec+float3( z,-z,-z));
half4 shadows = (shadowVals < mydist.xxxx) ? _LightShadowData.rrrr : 1.0f;
return dot(shadows,0.25);
#else
float dist = SampleCubeDistance (vec);
return dist < mydist ? _LightShadowData.r : 1.0;
#endif
}
#endif // #if defined (SHADOWS_CUBE)
同样,如果使用软阴影会采样四次并取平均值,硬阴影采样一次。同时没有进行过滤计算,计算昂贵且效果很粗糙!
对于点光源阴影实在不能用于手机平台,替代方式就是用无阴影点光+cookie投射,模拟阴影。或者用较少的聚光灯阴影代替。


















































