接上一期:

膜力鸭苏蛙可:引擎搭建记录(5) - 距离场 : 建场​zhuanlan.zhihu.com

项目地址:

MrySwk/GravityEngine​github.com

上一期,我们完成了距离场的构建,本文接下来分析并完整地实现一套类似于堡垒之夜中使用到的阴影系统,如下图。

近处两层级联,远处距离场

最远的大桌子是距离场阴影,近处的两个是级联/pcss(因为没有AO所以桌角还是有明显的漂浮感,不过这不是这篇文章的讨论范围)。


CSM与PCSS

因为不是在unity这类的引擎里面实现,要考虑的东西就多了很多,事实上我现在这个小轮子里CPU端只有一个线程负责update和发dc,再加上系统里还没有接入LOD的库,导致一旦场景变复杂,效率会变得非常低,后期可能需要重构一次把渲染线程和更新线程分出来,以及加上LOD,场景的复杂度带来的代价才能压得下去,因此这里就不花时间讨论这一块,后面着手进行这一块的优化的话可能再另作分享。

级联和PCSS网上也有很多解释了,在下就不拿来水文章凑数了,下面我们简单提一下CSM/PCSS,再重点介绍距离场阴影的实现。

级联

级联的初衷是增加shadowmap的分辨率,一张贴图覆盖特别大的范围会导致效果很糟糕,所以考虑离镜头越近的地方用分辨率越高的shadowmap,越远的地方则分辨率越低,下图是一个按视锥划分的四层级联。

view space alignment

而这样做带来的问题是,一旦镜头转动,就会出现很严重的闪烁(想象一张网格纸在另一张网格纸上面转动的效果),所以一般划分级联是在光源的空间中划分:

light space alignment

这样划分的话,每一层级联之间的重叠会更大,一般我们在重叠区域会取更精细的一层作为结果,在接近交缝处则考虑混合。

在渲染完shadowmap后,我们把阴影投回屏幕上,下图是把各层级联可视化的效果

R,G,B分别表示前两层级联和距离场的范围

PCSS

Percentage Closer Soft Shadow是3A大作中常见的软阴影解决方案,和卷积sm、esm、vsm这些技术相比,PCSS能更好的表现阴影的软硬变化,其通过距离比计算出半影大小,再进行PCF滤波(也可以不选择PCF而是和其他软阴影技术结合使用)。PCSS主要解决的是,离阴影接触点越近,阴影越硬的问题。

PCSS的半影大小(软硬程度)是由下图方式计算得到。

dBlocker的求法是,采样周围的像素,找有没有哪个点对当前这个点造成了遮蔽,如果有,那么根据这个blocker来算半影的大小,然后再用比较大的核去采样,如果不在半影范围内的,则不需要使用大的采样核,离blocker近的也使用小的采样核,这样就可以使得接触点近处的阴影硬,远的阴影软。

因为代码比较长所以就不列了,毕竟不是本文重点,此外,这一步可以考虑用比较低的采样数+temporal过滤,不过这种方法也不适合特别大的采样核(因为变化过大会导致被clip/clamp掉),带temporal的效果我也在代码中也实现了,但是效率似乎不如直接加大采样数,因此最后这个效果中没有开启prefilter也没有开启temporal,采样分布也非常naive地用了Hammersley,最终消耗加上后面的SDF March不会超过0.6ms。


实现距离场阴影

级联是非常贵的,不作任何处理的话,每层级联都意味着drawcall翻倍。其实,对于现代cpu来说,dc数量已经不是最大的问题了,相比之下,顶点、填充的开销可能会更严重。而距离场绝对不仅仅是为了阴影更软,还可以在保持动态的情况下降低级联的开销。

很多游戏里会用非常近的级联,然后远处用别的技术代替,比如far cry以前就用过高度阴影,虚幻里针对地形也做了个高度场,此外还有更直接的用static shadowmap的办法,用静态的坏处是,光源就不能动了,而tod在现在3A中是很常见的需求,此外,远处的阴影也就只有静态物体有了,其实对于大多数线性流程的游戏,烘焙的阴影也是完全够用的,但是对于沙盘之类要求比较高的游戏,静态可能会不太够用,对于这种情境,SDF不能算是唯一的解决方案,但绝对是非常decent的一种。

距离场阴影效率远远高于普通shadowmap阴影,全屏1spp的ray march,在pc平台上是完全可以接受的,实际上因为是远处,降半分辨率也不会有太大影响,顽皮狗在美末中甚至用过1/4分辨率的cone trace来做软阴影。

下图给出的是1080p全分辨率全屏trace(不是只trace远处),对1000个距离场进行march的结果,场景如下。

测试场景

首先需要提一下的一点是,用的是32x32x32分辨率的距离场(也是虚幻中默认的距离场大小),march的效率和网格多边形数量无关,只和分辨率有关,所以不要在意都是box,复杂物体的效率也是一样的。

结果剔除用了0.694ms,trace用到的耗时为0.669毫秒,因为没有随机,1spp画面也是稳定的,不需要做任何高斯、双边、temporal。

剔除耗时
trace耗时(不要在意名字)

看上去是1.3ms~1.4ms的效率,但是分tile剔除本身是一个非常偏vgpr consuming的pass,这意味着,在主机和pc的A卡上,如果async安排的好,那么这个pass基本上是免费的,代价基本上就是后面的这个0.67ms的ray march,而实际使用的时候只有远处,不是全屏,一般来讲级联还是会覆盖屏幕上大部分区域,所以march的开销会在0.3ms以内。

  • 分Tile剔除

上一期我们烘好了任意网格体的距离场:

烘出来的距离场(色带是因为march步长较大)

对距离场进行march之前,我们要确定当前这个像素点会交到哪些距离场,因为march的方向是固定朝着光源方向进行,所以我们从光源的方向进行分块剔除是一个比较合适的思路。

这一步我取的是64x64的分块(瞎取的),没有用距离场的AABB去求交,为了效率故直接用了sphere bound去求交,求交没有投影到灯光的viewport里进行,而是直接在view space,用的最粗糙的剔除,代码也很简单:

   for (int i = 0; i < gSceneObjectNum; i++){center = gSceneObjectSdfDescriptors[i].objLightSpaceCenter.xy;radius = gSceneObjectSdfDescriptors[i].objWorldRadius;xmin = tileXmin - radius;xmax = tileXmax + radius;ymin = tileYmin - radius;ymax = tileYmax + radius;if (center.x > xmin &&center.x < xmax &&center.y > ymin &&center.y < ymax){gSdfList[tileIndex].SdfObjIndices[gSdfList[tileIndex].NumSdf] = i;gSdfList[tileIndex].NumSdf++;}}

分块的效果和剔除的结果可视化一下,为了看的清楚点,就用比较少的物体演示下,用灰色代表1个,白色代表2个,如图所示:

  • Ray March

剔除完了之后,就可以开始cone trace了,trace的原理上一期也简单提过,这里再解释一次:

我们选定一个cone的角度,这个cone的角度越大,阴影越软,选定好后,沿着cone的中心轴步进,采样到的距离场值,就是其位置到最近点的距离,我们要找的是这跟中心轴上被遮蔽得最厉害的点,也就是(距离/圆锥半径)的最小值:

float dist = gSdfTextures[sdfInd].Sample(basicSampler, pos).r;
shadow = min(shadow, saturate(dist / (totalDis * CONE_TANGENT)));

march的距离我们知道,乘上tangent就是圆锥的半径,而距离就是距离场里采样的值,这样取到的最小值,就是阴影的值。

追踪的这一部分代码我大半都是嫖虚幻的。这里把我的代码贴一下,并解释一下大致原理:

#if !DEBUG_CASCADE_RANGE[branch]if (!cascaded){float3 lightDir = -normalize(gMainDirectionalLightDir.xyz);float3 opaqueWorldPos = worldPos;float3 rayStart = opaqueWorldPos + lightDir * RAY_START_OFFSET;float3 rayEnd = opaqueWorldPos + lightDir * MAX_DISTANCE;float2 tilePos = mul(float4(opaqueWorldPos, 1.0f), gSdfTileTransform).xy;int2 tileID = int2(floor(tilePos.x * SDF_GRID_NUM), floor((1.0f - tilePos.y) * SDF_GRID_NUM));int tileIndex = tileID.y * SDF_GRID_NUM + tileID.x;if (tileID.x < 0 || tileID.x >= SDF_GRID_NUM ||tileID.y < 0 || tileID.y >= SDF_GRID_NUM)return 1.0f;int objectNum = gSdfList[tileIndex].NumSdf;float minConeVisibility = 1.0f;[loop]for (int i = 0; i < objectNum; i++){uint objIndex = gSdfList[tileIndex].SdfObjIndices[i];int sdfInd = gSceneObjectSdfDescriptors[objIndex].SdfIndex;float3 volumeRayStart = mul(float4(rayStart, 1.0f), gSceneObjectSdfDescriptors[objIndex].objInvWorld).xyz;float3 volumeRayEnd = mul(float4(rayEnd, 1.0f), gSceneObjectSdfDescriptors[objIndex].objInvWorld).xyz;float3 volumeRayDirection = volumeRayEnd - volumeRayStart;float volumeRayLength = length(volumeRayDirection);volumeRayDirection /= volumeRayLength;float halfExtent = gMeshSdfDescriptors[sdfInd].HalfExtent;float rcpHalfExtent = rcp(halfExtent);
#if USE_FIXED_POINT_SDF_TEXTUREfloat SdfScale = halfExtent * 2.0f * SDF_DISTANCE_RANGE_SCALE;
#endiffloat3 localPositionExtent = float3(halfExtent, halfExtent, halfExtent);float3 outOfBoxRange = float3(SDF_OUT_OF_BOX_RANGE, SDF_OUT_OF_BOX_RANGE, SDF_OUT_OF_BOX_RANGE);float2 intersectionTimes = LineBoxIntersect(volumeRayStart, volumeRayEnd, -localPositionExtent - outOfBoxRange, localPositionExtent + outOfBoxRange);[branch]if (intersectionTimes.x < intersectionTimes.y){float sampleRayTime = intersectionTimes.x * volumeRayLength;uint stepIndex = 0;[loop]for (; stepIndex < MAX_STEP; stepIndex++){float3 sampleVolumePosition = volumeRayStart + volumeRayDirection * sampleRayTime;float3 clampedSamplePosition = clamp(sampleVolumePosition, -localPositionExtent, localPositionExtent);float distanceToClamped = length(clampedSamplePosition - sampleVolumePosition);float3 volumeUV = (clampedSamplePosition * rcpHalfExtent) * 0.5f + 0.5f;float distanceField;
#if USE_FIXED_POINT_SDF_TEXTUREdistanceField = SampleMeshDistanceField(sdfInd, SdfScale, volumeUV);
#elsedistanceField = SampleMeshDistanceField(sdfInd, volumeUV);
#endifdistanceField += distanceToClamped;// Don't allow occlusion within an object's self shadow distance//float selfShadowVisibility = 1 - saturate(sampleRayTime * selfShadowScale);//float sphereRadius = clamp(TanLightAngle * sampleRayTime, VolumeMinSphereRadius, VolumeMaxSphereRadius);//float stepVisibility = max(saturate(distanceField / sphereRadius), selfShadowVisibility);float sphereRadius = CONE_TANGENT * sampleRayTime;float stepVisibility = saturate(distanceField / sphereRadius);minConeVisibility = min(minConeVisibility, stepVisibility);float stepDistance = max(abs(distanceField), MIN_STEP_LENGTH);sampleRayTime += stepDistance;// Terminate the trace if we are fully occluded or went past the end of the rayif (minConeVisibility < .01f ||sampleRayTime > intersectionTimes.y * volumeRayLength){break;}}// Force to shadowed as we approach max stepsminConeVisibility = min(minConeVisibility, (1 - stepIndex / (float)MAX_STEP));}if (minConeVisibility < .01f){minConeVisibility = 0.0f;break;}}shadow = minConeVisibility;#if DEBUG_TILE_CULLINGshadow = gSdfList[tileIndex].NumSdf / 2.0f;//shadow = (float)tileIndex / (SDF_GRID_NUM * SDF_GRID_NUM);
#endif}
#endif

首先我们根据深度还原世界坐标,然后投到light space里找到对应的tile,查看tile里面存的距离场,找到之后开始march,先发送一条光线和距离场的AABB求交,确定march的起点和终点,这里这个AABB可以稍微扩大一圈,以免阴影特别软的时候参与march的范围不够。

接下来规定一个最大步长,我这里取了64步,然后对于距离场外的位置,我们直接clamp到距离场内,并且加上clamp的距离作为距离场采样值,在距离场内的就直接采样,然后就是按上面所说的步进、采样,找最小的遮蔽系数作为阴影的值。

遇到负数,说明是本影,就可以early out,遇到超出边界,也可以直接停止march,这样,我们就得到了软阴影的效果:

  • 作为近处使用的主要阴影系统的可能性?

距离场也有一些非常麻烦的问题,降低了距离场的实用性,首先就是烘焙慢,一万面左右的模型,一个32分辨率的场大概要烘十来秒到一分钟不等,128分辨率的经常五分钟起步,对于比较大的项目而言,这会是一个比较大的开支,有的时候可以考虑只烘焙比较大的建筑物等。此外,显存占用也是一个问题,比如在UE4里开启距离场会吃掉300M的显存。

烘焙速度慢,也就注定了距离场不能支持顶点动画,只能烘死了不做动画,物体级别的移动、旋转、缩放等是支持的,我们只需要对每个距离场维护一个变换矩阵就可以了,但是如果不能做动画,使用就非常受限了,所以,我们可以考虑用胶囊体等简单形状来做动画。

胶囊体阴影

虚幻里很早就支持了胶囊体软阴影,胶囊体做阴影的效果会远远软于PCSS,发个文档链接:

胶囊体阴影​docs.unrealengine.com

虚幻给的图里效果不是很好,实际上用起来效果是相当不错的,顽皮狗在美末里以前也有过这个思路,那就是用简单的胶囊等几何体代替人来做复杂的trace计算:

出来的效果非常软非常好看,简直吊打shadowmap:

胶囊体软阴影

胶囊体、box、圆柱、圆环等大多数简单的几何形体,其距离场都可以用公式表达,这个网页里收录了很多距离场公式:

fractals, computer graphics, mathematics, shaders, demoscene and more​www.iquilezles.org

胶囊体的距离场函数:

float SampleMeshDistanceField(int sdfInd, float3 uv)
{float dist = gSdfTextures[sdfInd].SampleLevel(basicSampler, uv, 0).r;return dist;
}float SampleMeshDistanceField(int sdfInd, float SdfScale, float3 uv)
{float dist = gSdfTextures[sdfInd].SampleLevel(basicSampler, uv, 0).r;dist = (dist - 0.5f) * SdfScale;return dist;
}

march的过程和上面一样,可以写个简单的看下效果:

这个trace是只沿光源方向,不包括环境光,所以可以看到阴影内还是非常flat,这种trace的方式也和上面图中的不同,是存在比较大的本影区域的。

虚幻里有个专门给胶囊体用的CapsuleShadow,是CapsuleShadowShaders.usf这个文件,感兴趣的话各位可以去看,实现思路也基本上是先分tile剔除,再march距离场。

总结

距离场阴影和shadowmap相比,没有drawcall,代价非常小,效果远远软于PCSS,可以在没有额外代价的情况下调整阴影的软硬程度(可以特别特别软),配合胶囊体可以做动画,代价是烘焙需要时间,吃显存,总的来说是一项非常实用的阴影技术。

再次附上代码实现地址:

MrySwk/GravityEngine​github.com

Reference

[1] fractals, computer graphics, mathematics, shaders, demoscene and more

[2] http://miciwan.com/SIGGRAPH2013/Lighting%20Technology%20of%20The%20Last%20Of%20Us.pdf

[3] Cascaded Shadow Maps - Win32 apps

[4] https://www.realtimeshadows.com/sites/default/files/Playing%20with%20Real-Time%20Shadows_0.pdf

[5] https://developer.download.nvidia.cn/shaderlibrary/docs/shadow_PCSS.pdf

[6] https://github.com/chenjd/Unity-Signed-Distance-Field-Shadow

[7] https://github.com/EpicGames/UnrealEngine

visio任意区域填充斜线阴影_DX12渲染管线(6) - 级联阴影与距离场阴影相关推荐

  1. UE4-(室外光照)距离场阴影

    对于级联阴影贴图的局限性(级联距离值较大,阴影模糊,性能消耗较大,级联距离值较小,阴影清晰但是远处的几何体阴影消失),可以考虑使用距离场阴影. 注意:距离场阴影在工程中不会默认开启,需要手动设置,设置 ...

  2. 室外场景注意事项(一)距离场阴影的利弊!

    好嘛,沉寂多年,又开始写一些内容了.主要是针对近期又开始做一些有关UE 性能优化的内容,不可不开始把自己的感悟记录下来. 刚开始接触距离长的时候是官方的一篇文章,是作为及知乎上一个大佬通过距离长对动态 ...

  3. (145)光线追踪距离场柔和阴影

    光线追踪距离场阴影利用场景距离场展示的属性用于计算动态网格体的有效区域阴影.这使用的数据和 距离场环境遮挡 相同,因此许多限制也相同. 为计算阴影,从场景定向距离场被遮挡的点到每个光源进行光线追踪.使 ...

  4. ui设计卡片阴影_UI设计形状和对象基础知识:阴影和模糊

    ui设计卡片阴影 第三部分 (Part 3) Welcome to the third part of the UI Design super-basics. This time we'll cove ...

  5. 关于阴影映射的那些事,shadow acne(阴影失真)和peter panning(阴影悬浮)

    在之前学习阴影映射时看的是这篇文章https://learnopengl-cn.github.io/05 Advanced Lighting/03 Shadows/01 Shadow Mapping/ ...

  6. html阴影和圆角边框,css3圆角边框和边框阴影示例

    border-radius向元素添加圆角边框,css3中的.IE9+ chrome safari5+ firefox4+ 现在都支持.可以向input div等设置边框.与border相似,可以四个角 ...

  7. box-shadow的模糊距离和阴影扩展半径的关系

    参数说明 box-shadow: 2px 2px 4px 1px red insert; 参数从左到右依次是,水平阴影位置(必须).垂直阴影位置(必须).模糊距离(可选).阴影扩展半径(可选) 阴影颜 ...

  8. 如何利用Visio来绘制斜线!

    转自 http://blog.sina.com.cn/s/blog_b0b8d463010171it.html 具体步骤 1.确定好要画斜线的首尾端点,先只画折线: 2.选中折线->右键-> ...

  9. visio任意插入word中的数学公式

    第一步.复制word中的公式 直接选着右击 '复制',或者 'Ctrl+C' 第二步.到visio的空白的地方 鼠标右键,点击 '选择性粘贴',然后选择:Microsoft World 文档,点击确定 ...

最新文章

  1. linux图形登陆报拒绝权限,Linux-权限被拒绝?
  2. jenssen不等式的证明
  3. 使用jmeter测试java程序
  4. phpinfo信息泄漏
  5. webwork2.2.4+spring配制方式总结
  6. 581. 最短无序连续子数组
  7. Java NIO 教程
  8. View Programming Guide for iOS官方文档翻译一
  9. ERROR 1071 (42000): Specified key was too long; max key length is 767 bytes
  10. 技术水平低,就这还敢写自动化项目实战经验丰富?
  11. Linux多台机器配置ssh免登录
  12. 使用Nginx制作下载站点
  13. Jedis操作reids集群
  14. .net core 图片合并,图片水印,等比例缩小,SixLabors.ImageSharp
  15. 网易邮箱服务器怎么注册,免费网易域名邮箱申请教程(图)
  16. gitee错误: remote gite already exists.
  17. [Power--IC]电源管理IC-STNS01
  18. LayUI项目之我的会议(送审以及排座)
  19. 【读懂Autosar代码】-1-概述
  20. 电脑可以上网但网络显示感叹号无Internet的解决办法

热门文章

  1. 非阻塞模式WinSock编程入门
  2. 网骗欺诈?网络裸奔?都是因为 HTTP?
  3. 科普 | 什么是Service Mesh技术?
  4. Mark一下 | 当当优惠码,实付满150减30 | + 荐书
  5. 如何从零开始写一个 web 框架?
  6. 优化传输文件的性能- -零拷贝
  7. 李松南:智能全真时代的多媒体技术——关于8K、沉浸式和人工智能的思考
  8. 编码服务正在步入云端
  9. 展望2018:WebRTC和下一代编解码器
  10. 腾讯陈妍:万物互联时代,保险业应更注重服务创新来挖掘用户需求