五一假期来了然鹅并不能出去,自己也在家呆了一个月了(虽然居家办公很爽但是生活上多了很多不便),希望疫情早日结束。
HomeWork1总结了现代实时渲染里常用的Real-Time Shadow实现方法,本篇以GAMES202作业1为基础,再探讨一些更现代的做法。顺便也把此博客当做一次知识笔记记录一下课堂内容,彻底的弄清楚每个知识点。
GitHub Link:
https://github.com/Nuomi-Chobits/GAMES202-Unity-GRP
HomeWork1目标
- 建立最基础的ShadowMap渲染管线
- SM
- PCF(Percentage Closer Filter)
- PCSS(Percentage Closer Soft Shadow)
- 扩展/补充一些更现代的Real-Time Shadow实现方法
HomeWork1
1.Shadow Mapping
(1)最基础的2遍Pass的算法:
第一遍从光照角度(假设灯光位置有一个虚拟的相机)看向场景,记录场景最近的深度,生成SM
第二遍从相机位置出发,比较深度,生成阴影。
SM是一个完全基于Texture的算法,自然具备了Texture的一些优缺点:
优点:不需要场景物体几何信息
缺点:self occlusion and aliasing
一些细节:
第一遍生成SM的时候实际上我们构造的相机通常是透视相机。因为是虚拟相机,自然会有相机的裁剪剔除等一系列操作,所以一般会设置一个最大的SM距离。
第二遍比较深度的时候我们通常是向GPU传输我们的VP矩阵,比较VP变化后的z值,当然也可以统一比较linear depth。在GRP中我们是调用SRP来计算Light的VP矩阵。
cullingResults.ComputeDirectionalShadowMatricesAndCullingPrimitives(
light.visibleLightIndex, 0, 1, Vector3.zero, tileSize, 0f,
out Matrix4x4 viewMatrix, out Matrix4x4 projMatrix, out ShadowSplitData shadowSplitData
);
dirShadowMatrices[index] = CalcLightMVP(
projMatrix ,
viewMatrix,
SetTileViewport(index, split, tileSize),
split
);
cmd.SetViewProjectionMatrices(viewMatrix, projMatrix);
..
//发送数据到GPU
cmd.SetGlobalMatrixArray(dirShadowMatricesId, dirShadowMatrices);
shader:
采样SM:
#define SHADOW_SAMPLER sampler_linear_clamp_compare
TEXTURE2D_SHADOW(_DirectionalShadowAtlas); SAMPLER_CMP(SHADOW_SAMPLER);
对于上面这段代码不熟悉dx或者unity宏的同学看了估计会很困惑,我看了很多搬运catlike srp的文章直接就机器翻译过来了并没有做解释,所以这里给大家解释一下:
sampler_linear_clamp_compare 是一个采样器状态宏,https://docs.unity3d.com/Manual/SL-SamplerStates.html
sampler_linear_clamp_compare 就代表了采样这张SM时, texture filtering mode 为 “Linear”,wrap mode为“Clamp”
而compare实际上代表着是Direct3D 10之后的一个功能SamplerComparisonState,类型为
SampleCmpLevelZero(等效于 SampleCmp(mipmap level0))。
不过我不明白的是既然catlike佬说明了sm不需要常规的bilinear filtering,为什么写的时候还要开linear而不是point呢?为此我还查了一下urp的代码,只在metal平台下启用了point filtering,那可能是其他平台有坑点吧。(后续:亲自去dx、ogl测试了一下,dx下能明显看到point和linear差异,说明底层调用硬件层面时做了针对性的差异,ogl下默认linear,更改宏并不奏效)这个问题我就放在后面PCF再解答吧。
float SampleDirectionalShadowAtlas(float3 positionSTS) {
return SAMPLE_TEXTURE2D_SHADOW(
_DirectionalShadowAtlas, SHADOW_SAMPLER, positionSTS
);
}
float GetDirectionalShadowAttenuation(DirectionalShadowData data, float3 positionWS) {
if (data.strength <= 0.0) {
return 1.0;
}
float3 positionSTS = mul(
_DirectionalShadowMatrices[data.tileIndex],
float4(positionWS, 1.0)
).xyz;
float shadow = SampleDirectionalShadowAtlas(positionSTS);
return lerp(1.0, shadow, data.strength);
}
至此,最简单SM就实现了。但是出现了很严重的shadow acne,这个产生的原因GAMES101和202都有讲过,不再赘述,一图概括:
出现这个问题是因为采样造成的,那么加大SM的分辨率有用吗?
这实际上是一个信号处理的问题,用更多、更密集的离散的采样点来采样一个连续值,实际上还是会存在信号丢失的问题。
这个问题可以调整depth bias来缓解
cmd.SetGlobalDepthBias(30000f, 0f);
//或者
cmd.SetGlobalDepthBias(0f, 3f);
但是当bias过大时会引起另一个问题(下图为了使现象更明显调大了很多bias):
上诉问题我们通常的解决方法是寻找一个合适的数值。除了上诉问题外,SM不可避免的还有锯齿问题,解决思路有两个:一个是增大SM的分辨率,另一个就是引入AA。CSM就是一种增大SM分辨率的思路,但是GAMES202作业中里并没有安排这个内容的作业,所以先把CSM的内容作为拓展部分放在后面了。
2.Percentage Closer Filter(PCF)
先说什么是PCF:从上文SM的结果来看,可以看到在边缘有很明显的锯齿。有一个解决思路是SM经过深度比较后记录的实际上是一个非零即一的数值,人们就想到对周围的像素点的结果加权平均得到一个不那么强烈锯齿的结果。
所以PCF刚被创造出来时是为了解决SM边缘的走样问题。PCF既不是对SM模糊,也不是在场景上做模糊。
前文提到过为什么不使用sampler_point_clamp_compare,这正是因为启用sampler_linear_clamp_compare代表着我在硬件层面采样时使用SampleCmpLevelZero,相当于做了一次PCF1x1:(当SM采用RenderTextureFormat.ShadowMap格式时,对应一种硬件可直接读取的 RenderTexture 格式,使用这种格式的好处是硬件对于这种格式可以自行进行 PCF,不需要我们在着色器中实现 PCF,优势是硬件内置的 PCF 比自己在着色器中实现性能要好。要得到正确的 pcf,filterMode 必须是 Bilinear,而不能是 Point。但是RenderTextureFormat.ShadowMap这种格式并不是所有硬件都支持。)
catlike佬说明了sm不需要常规的bilinear filtering:这是因为sampler_linear_clamp_compare一般代表着d3d11下filter为D3D11_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT,并不是常规的D3D11_FILTER_MIN_MAG_MIP_LINEAR或者其他:https://docs.microsoft.com/zh-cn/windows/win32/api/d3d11/ne-d3d11-d3d11_filter。
由于catlike的教程中直接用了Unity原生的pcf,所以我特地写了一篇Unity的PCF文章http://tajourney.games/5482/,此处就不再细讲了。感兴趣的同学可以去看下。