【RSM】Reflective Shadow Map

"Perfect is the enemy of good". If it works, it is good enough.

常在一些技术分享中看到RSM的身影,此前一直以为是跟其他XSM类似的阴影绘制方案,没有放在心上,最近在阅读一篇博客的时候才知道这个认知并不完全正确,为了做个总结,同时给其他有类似的想法的同学做一个分享,这里将博客中的关键内容分享出来,博客的链接在参考部分有给出。

1. 基础概念

RSM是对标准Shadow Map方案的一种改进方案,改进在于这个方案不但兼有标准Shadow Map方案的阴影输出功能,同时还能提供一定的间接光效果(只包含间接光中的单次diffuse反射-color bleeding效果),这也是前面为什么说此前认知部分正确的原因。

我们知道,标准Shadow Map只需要一张depth texture就够了,而放到RSM中,这个数据就没这么简单了,除了用于实现直接光阴影绘制的depth texture之外,还同时需要提供depth texture上对应像素的world position数据(虽然可以从depth推算出来,但是这里先按照原博客中的说法,直接存储在贴图中)、world normal数据(也可以根据depth换算一个近似值)以及当前点的反射光照的flux数据(通常用当前像素的albedo乘上光照在此像素位置的光强即可,这个数值是后面计算此像素作为point light提供给相机视角下的像素的照明使用的),下图从左往右依次是depth、world position、world normal、flux(RGB)的结果,图来自于原博客。

2. RSM的生成

因为这里突然增加了很多数据,标准SM方案中不需要PS参与的优秀特性这里就不能传承了,需要一个单独的PS来完成Multi Render Target的输出(这个过程需要实时完成,带宽的消耗使得目前大部分手机都很难支持这个效果)。

这里同样使用的是基于光源视角的绘制,对于场景中如果有多盏光源需要添加对应效果,那么就需要生成多套RSM数据,对于方向光跟聚光灯而言,就只有一套RSM即可,对于点光,则需要生成一套RSM cubemap。

RSM对应的数据的生成过程这里就不展开了,也没有太过复杂的技术细节,从前面的文字描述中应该能大致清楚其中的实现逻辑了。

3. RSM的使用

RSM的使用需要占用一个PostProcess Pass,全屏的(出于提升性能考虑,可以使用半分辨率进行,后续上采样到全分辨率即可),这里需要注意的是,方向光等全范围覆盖的光照除外,为了减少fillrate的消耗,其他局部光源的lighting部分会通过绘制一个光源光照范围的volume mesh之后在PS中完成计算,而这里RSM的应用是全屏幕空间都需要进行的,不管这个像素有没有被光照volume所覆盖,因此不能将RSM的计算跟非方向光之外的其他光源的direct lighting部分放在一起完成。

RSM具体是如何使用的呢,对于屏幕空间的每个像素而言,需要对每套RSM进行一次计算处理,最粗暴的方法就是将RSM中的每个像素都当成是一个point light,完成光照的计算与累加,但是这种做法消耗无疑是扛不住的。

优化一点的算法则是,对于每个像素而言,会选取若干个随机的二维坐标(用于RSM采样)作为采样点(可以基于这样的观察结果来选择,即距离当前像素最近的VPL,其贡献越大,之后基于范围进行筛选,并在筛选结果中基于随机算法进行选取)。

如上图所示,Games104总结了采样点选取的一些细节事项,可以作为此处简陋描述的补充,这里也说到了:RSM只需要 400 次(每个像素只需要采样400个周围的RSM?)采样就能得到 1 bounce 的 GI 的效果。

得到的每个采样点可以看成是一盏点光,完成对这个像素的光照计算,为了提高计算效率,随机坐标可以提前计算好并固定下来,原文中介绍这种做法除了可以降低计算消耗之外,还有助于减轻锯齿(所有像素使用相同的采样pattern,会不会出现摩尔纹……),算法的实现细节这里就不展开了,直接偷懒粘贴一下原博中的实现HLSL:

float3 DoReflectiveShadowMapping(float3 P, bool divideByW, float3 N)
{
  float4 textureSpacePosition = mul(lightViewProjectionTextureMatrix, float4(P, 1.0));
  if (divideByW) 
    textureSpacePosition.xyz /= textureSpacePosition.w;

  float3 indirectIllumination = float3(0, 0, 0);
  float rMax = rsmRMax;

  for (uint i = 0; i < rsmSampleCount; ++i)
  {
    float2 rnd = rsmSamples[i].xy;

    float2 coords = textureSpacePosition.xy + rMax * rnd;

    float3 vplPositionWS = g_rsmPositionWsMap.Sample(g_clampedSampler, coords.xy).xyz;
    float3 vplNormalWS = g_rsmNormalWsMap.Sample(g_clampedSampler, coords.xy).xyz;
    float3 flux = g_rsmFluxMap.Sample(g_clampedSampler, coords.xy).xyz;

    float3 result = flux
    * ((max(0, dot(vplNormalWS, P – vplPositionWS))
    * max(0, dot(N, vplPositionWS – P)))
    / pow(length(P – vplPositionWS), 4));

    result *= rnd.x * rnd.x;
    indirectIllumination += result;
  }
  return saturate(indirectIllumination * rsmIntensity);
}

鉴于GI数据的低频性,我们这里还可以在半分辨率甚至更低分辨率下进行GI的计算:

  1. 每隔两个或者每隔四个pixel做一次GI计算
  2. 未直接进行GI计算的像素可以共用周围像素计算的结果
  3. 如果(RSM)采样点和当前待计算GI的像素在空间位置上相差很大,或者法线朝向不共面,会产生Artifacts(不过存在这类渲染错误的pixel不会很多,对于整个屏幕来讲可能不到 1% 甚至千分之一),遇到这种情况,可以认为这是个无效的值,只需重新对它进行一次完整的采样即可。

这个方案在很多3A大作上是启用的,可以参考上图手电筒开启时的效果比对。

4. RSM结果

这里也直接贴上一张原博的实现效果图:

左图是标准SM的效果,中图是RSM的效果,右图是两者的差值。

从RSM效果来看,有如下一些不同之处:

  1. 兔子身上有了周围环境反射的color bleeding效果
  2. 阴影更为柔和(可以通过计算被shadowed的像素到cast shadow的像素之间的距离来调节这个强度因子实现更为自然效果),不像左图那样死黑
  3. 由于增加了单次diffuse反射效果,因此整体亮度变高了

5. RSM特点

优点

  • 实现简单

不足

  • 只支持 Single Bounce
  • 计算过程中没做VPL可见性的检测
  • 没有考虑间接光照的遮挡(AO效果?)

参考

[1] Reflective Shadow Maps
[2] Reflective Shadow Maps: Part 2 – The implementation
[3] Reflective Shadow Maps原论文
[4]. 虚幻5的Nanite和Lumen实现机制是什么?- Games
104

你可能感兴趣的:(【RSM】Reflective Shadow Map)