找回密码
 立即注册
查看: 168|回复: 0

UE5衬着管线--ShadowPass通道与VSM

[复制链接]
发表于 2024-7-15 18:25 | 显示全部楼层 |阅读模式
前言:本篇是继“UE5衬着前数据措置”专栏之后,衬着管线系列之ShadowPass通道篇。
1、ShadowDepthMap是什么

以光源为不雅察看点(摄像机)去衬着场景,运用 UE5衬着管线--PrePass通道 的道理衬着得到该光源的深度图,即ShadowDepthMap,以下简称ShadowMap。而ShadowPass就是为每个需要投射暗影的光源生成ShadowMap,供后续Lighting阶段采样ShadowFactor,生成暗影效果。
2、ShadowMap道理


  • 遍历需要生成ShadowMap的光源,如果是点光源需要对6个标的目的生成ShadowMap,输出到CubeMap上。
  • 按正常衬着,我们是以摄像机视角去不雅察看场景,这样就能知道该摄像机能看到什么,不能看到什么;
  • 采用2的道理,以光源标的目的,对场景进行不雅察看,输出该光源可以看到(照亮)的部门,而这个记录的东西就是ShadowMap;
  • 在着色阶段,衬着某个Pixel时,将该点Position转换到某光源空间,与对应记录的ShadowMap对比,判断光源对该点是否可见,不成见即处于暗影中。
保举闫令琪图形学的学习资料,进一步学习:
暗影方案的迭代:

  • 传统ShadowMap:直接在对ShadowMap采样计算暗影,受限于分辩率会存在暗影掉真的问题;
  • CSM:首先在视锥体深度z上划分分歧的区域,距离摄像机越远的区域生成分辩率越高的暗影图,然后对每个区域计算投影矩阵、生成分歧分辩率的暗影图。采样时,分歧级别之间的过度使用三线性插值过度;
  • PCF:暗影抗锯齿方案,硬件提供一次2x2=4点采样,一次采样4个值并返回平均值,道理上是对锯齿滤波,模糊了锯齿但也可能丢掉高模的细节。需要较多采样效果才抱负,即使有硬件撑持,我们需要4x4=16区域,也需要16/4=4次采样。
  • PCSS:软暗影方案,通过遮挡物与暗影的距离来决定PCF滤波的大小,从而发生远处更模糊(软)暗影。采样次数比PCF多,平均深度的采样计算再加上PCF的采样。
  • VSSM:Variance Soft Shadow Mapping,该方案是对PCSS的一种近似优化,假设深度分布为正态分布,以此解决PCSS采样开销的问题;
  • VSM:Virtual Shadow Maps虚拟暗影贴图,UE目前保举的暗影方案,参考 官方文档 。而Shadow Map Ray Tracing(SMRT) 则是一种使用VSM的采样算法,能生成更为真实的柔和暗影和接触硬暗影。物体投射的远处暗影比近处暗影拥有更柔和的效果。
3、VSM简析

VSM相关的类实此刻VirtualShadowMapArray.h\cpp里,以下总结关键点。
VSM拥有一个全局的PageTable和一个一维的VirtualTexture Buffer,引擎中所有的VSM通过这个PageTable映射到真正的Physical Page上。代码实现上,每个VSM实例都有这个全局PageTable的引用。
PageTable是一个uint数组,每一个元素暗示一个编码过的page信息,通过下图的ShadowDecodePageTable方式解码为FShadowPhysicalPage类型:



获取某个page的信息



解码page信息



page信息类型FShadowPhysicalPage

得到Page信息后,就可以获取该Page指向的实际物理地址(FShadowPhysicalPage.PhysicalAddress)了,从而访谒到真实的纹理数据,即ShadowDepthMap的值。
标的目的光是一个全局范围的,以前采用CSM来措置远近分辩率的适配问题,UE在VSM基础上使用Clipmap来措置标的目的光的分辩率适配问题。Clipmap的感化是用尽量少的内存衬着更大的空间。用多层等大分辩率的数据,每一层覆盖空间的一个范围,越高层覆盖的范围越大,每一层大小是上一层的2倍。每个标的目的光对应一个FVirtualShadowMapClipmap类,Clipmap的多个层级数据保留在FLevelData布局中。


每层Level都有一个VSM,它们的Mipmap都只有0级的。每个Level的VSM分辩率都一样,只是覆盖范围纷歧样,覆盖范围越小越精细。0级覆盖半径是64,后面N级都是前一级的2倍。采样时通过计算该点与Level中心的距离来决定采样哪个层级,从而实现场景中1像素对应1纹素的效果。
在FVirtualShadowMapClipmap构造函数,可以看到LevelData的初始化,以下分析布局体初始化:
首先会从配置里获取到FirstLevel(默认6),LastLevel(默认22),从而计算出LevelData数组的长度:


FirstLevel以摄像机为中心,覆盖128x128的范围暗影;LastLevel则可以覆盖40平方千米的范围,满足开放大世界需求。
FLevelData.ViewToClip光源对应的不雅察看空间转换矩阵,将会在之后的ShadowPass中使用,外部调用方式为FVirtualShadowMapClipmap::GetProjectionShaderData


FLevelData.WorldCenter为该Level的中心,从摄像机Snap到Level Grid后转换回世界空间,代表着光源的位置,然后以此为Viewport的中心衬着VSM。
FLevelData.CornerOffset记录Clipmap偏移,用于2帧计算ClipmapCornerOffsetDelta,然后更新缓存的PageAddress:
  1. //VirtualShadowMapPageManagement.usf
  2. void CreateCachedPageMappings(uint VirtualShadowMapId, uint GlobalPageOffset, uint PageOffsetInSM, const bool bSinglePageSM)
  3. {
  4.         const int2 ClipmapCornerOffsetDelta = OffsetScale * ShadowMapCacheData[VirtualShadowMapId].ClipmapCornerOffsetDelta;
  5.         int2 PrevPageAddress = int2(PageAddress);
  6.         if (Projection.LightType == LIGHT_TYPE_DIRECTIONAL)
  7.         {
  8.                 // Clipmap panning
  9.                 PrevPageAddress += ClipmapCornerOffsetDelta;
  10.         }
  11. }
复制代码
VSM先分析到这里,接下来分析ShadowPass是怎么填充这些VSM数据的。

4、UE ShadowMap实现

初始化并分配需要衬着的VSM。InitViewsBeforePrepass,InitViewsAfterPrepass,InitDynamicShadows,BeginInitDynamicShadows这些方式城市执行一些VSM初始化操作,得到需要衬着的SortedShadowsForShadowDepthPass列表。下面是每帧执行的InitViewsAfterPrepass:
  1. void Render(FRDGBuilder& GraphBuilder)
  2. {
  3.         void FDeferredShadingSceneRenderer::InitViewsAfterPrepass(/**/)
  4.         {
  5.                 void FSceneRenderer::FinishInitDynamicShadows(/**/)
  6.                 {
  7.                         // 遍历光源,初始化添加需要衬着的VSM
  8.                         AllocateShadowDepthTargets(RHICmdList);
  9.                         // 收集VSM的Primitive列表
  10.                         // Generate mesh element arrays from shadow primitive arrays
  11.                         void GatherShadowDynamicMeshElements(DynamicIndexBuffer, DynamicVertexBuffer, DynamicReadBuffer, InstanceCullingManager)
  12.                         {//....
  13.                                 if (UseNonNaniteVirtualShadowMaps(ShaderPlatform, FeatureLevel))
  14.                                 {
  15.                                         // GPUCULL_TODO: Replace with new shadow culling processor thing
  16.                                         for (FProjectedShadowInfo* ProjectedShadowInfo : SortedShadowsForShadowDepthPass.VirtualShadowMapShadows)
  17.                                         {
  18.                                                 if (ProjectedShadowInfo->bShouldRenderVSM)
  19.                                                 {
  20.                                                         FVisibleLightInfo& VisibleLightInfo = VisibleLightInfos[ProjectedShadowInfo->GetLightSceneInfo().Id];
  21.                                                         ProjectedShadowInfo->GatherDynamicMeshElements(*this, VisibleLightInfo, ReusedViewsArray, DynamicIndexBuffer, DynamicVertexBuffer, DynamicReadBuffer, InstanceCullingManager);
  22.                                                 }
  23.                                         }
  24.                                 }
  25.                                 // 构建改VSM对应的MeshDrawCommand,绑定Shader,参数等
  26.                                 // FShadowDepthPassMeshProcessor,ShadowDepthVertexShader ShadowDepthPixelShader
  27.                                 SetupMeshDrawCommandsForShadowDepth(Renderer, InstanceCullingManager);
  28.                         }
  29.                 }
  30.         }
  31. }
复制代码
从以上代码注释得出,首先遍历所有光源,分配VSM,然后收集每个VSM View相关的Primitives,使用FShadowDepthPassMeshProcessor生成MeshDrawCommand,供后续GPU剔除,光栅化使用。
衬着入口函数:FSceneRenderer::RenderShadowDepthMaps,调用关系为:
  1. void FDeferredShadingSceneRenderer::Render(FRDGBuilder& GraphBuilder)
  2. {
  3.         void FSceneRenderer::RenderShadowDepthMaps(FRDGBuilder& GraphBuilder, FInstanceCullingManager &InstanceCullingManager)
  4.         {
  5.                 // Ensure all shadow view dynamic primitives are uploaded before shadow-culling batching pass.
  6.                 // 更新这些Shadow影响到的Primitive到GPUScene
  7.                 for (FProjectedShadowInfo* ProjectedShadowInfo : SortedShadowsForShadowDepthPass.VirtualShadowMapShadows)
  8.                 {
  9.                         Scene->GPUScene.UploadDynamicPrimitiveShaderDataForView(GraphBuilder, *Scene, *ProjectedShadowInfo->ShadowDepthView, ExternalAccessQueue, true);
  10.                 }
  11.                 // ....GPUScene其他Shadow更新
  12.                 InstanceCullingManager.BeginDeferredCulling(GraphBuilder, Scene->GPUScene);
  13.                 void FSceneRenderer::RenderVirtualShadowMaps(FRDGBuilder& GraphBuilder, bool bNaniteEnabled)
  14.                 {
  15.                         void FShadowSceneRenderer::RenderVirtualShadowMaps(FRDGBuilder& GraphBuilder, bool bNaniteEnabled, bool bUpdateNaniteStreaming, bool bNaniteProgrammableRaster)
  16.                         {
  17.                                 VirtualShadowMapArray.RenderVirtualShadowMapsNonNanite(GraphBuilder, VirtualShadowMapShadows, SceneRenderer.Views);
  18.                                 VirtualShadowMapArray.MergeStaticPhysicalPages(GraphBuilder);
  19.                         }
  20.                         // Render non-VSM shadows
  21.                         RenderShadowDepthMapAtlases(GraphBuilder);
  22.                 }
  23.         }
  24. }
复制代码
首先,RenderShadowDepthMaps()的前半部门,将光源影响到的Primitive更新到GPUScene,然后使用之前分析的文章 GPUScene与InstanceCulling裁剪 对这些Primitives开启延迟裁剪,降低后续衬着暗影的Overdraw。
然后,调用VirtualShadowMapArray.RenderVirtualShadowMapsNonNanite()方式执行衬着。
  1. void FVirtualShadowMapArray::RenderVirtualShadowMapsNonNanite(/**/)
  2. {
  3.         // 收集所有需要衬着的View:每个光源衬着ShadowDepth对应一个View,点光源6个方位对应为6个VSM的View
  4.         TArray<Nanite::FPackedView, SceneRenderingAllocator> VirtualShadowViews;
  5.         for (int32 Index = 0; Index < VirtualSmMeshCommandPasses.Num(); ++Index)
  6.         {
  7.                 FVSMCullingBatchInfo VSMCullingBatchInfo;
  8.                 VSMCullingBatchInfo.FirstPrimaryView = uint32(VirtualShadowViews.Num());
  9.                 VSMCullingBatchInfo.NumPrimaryViews = AddRenderViews(ProjectedShadowInfo, 1.0f, HZBTexture != nullptr, false, ProjectedShadowInfo->ShouldClampToNearPlane(), VirtualShadowViews);
  10.                 VSMCullingBatchInfos.Add(VSMCullingBatchInfo);
  11.                 InstanceCullingMergedContext.AddBatch(GraphBuilder, InstanceCullingContext, DynamicInstanceIdOffset, ShadowDepthView->DynamicPrimitiveCollector.NumInstances(), &PassParameters->InstanceCullingDrawParams);
  12.                 // 添加到衬着列表
  13.                 BatchedVirtualSmMeshCommandPasses.Add(ProjectedShadowInfo);
  14.         }
  15.         // Instance合批
  16.         InstanceCullingMergedContext.MergeBatches();
  17.         // 开始遮挡剔除
  18.         GraphBuilder.BeginEventScope(RDG_EVENT_NAME(”CullingPasses”));
  19.         FCullingResult CullingResult = AddCullingPasses(/**/);
  20.         // 光栅化,即衬着ShadowDepth
  21.         GraphBuilder.AddPass(
  22.                         RDG_EVENT_NAME(”RasterPasses”),
  23.                         PassParameters,
  24.                         ERDGPassFlags::Raster | ERDGPassFlags::SkipRenderPass,
  25.                         [PassParameters,
  26.                         BatchedPassParameters=MoveTemp(BatchedPassParameters),
  27.                         BatchedVirtualSmMeshCommandPasses=MoveTemp(BatchedVirtualSmMeshCommandPasses)](FRHICommandList& RHICmdList)
  28.                 {
  29.                                 FIntRect ViewRect;
  30.                                 ViewRect.Min = FIntPoint(0, 0);
  31.                                 ViewRect.Max = FVirtualShadowMap::VirtualMaxResolutionXY;
  32.                                 FRHIRenderPassInfo RPInfo;
  33.                                 RPInfo.ResolveRect = FResolveRect(ViewRect);
  34.                                 RHICmdList.BeginRenderPass(RPInfo, TEXT(”RasterizeVirtualShadowMaps(Non-Nanite)”));
  35.                                 RHICmdList.SetViewport(ViewRect.Min.X, ViewRect.Min.Y, 0.0f, FMath::Min(ViewRect.Max.X, 32767), FMath::Min(ViewRect.Max.Y, 32767), 1.0f);
  36.                                 // 遍历所有需要衬着的VSM
  37.                                 for (int32 Index = 0; Index < BatchedVirtualSmMeshCommandPasses.Num(); ++Index)
  38.                                 {
  39.                                         FParallelMeshDrawCommandPass& MeshCommandPass = ProjectedShadowInfo->GetShadowDepthPass();
  40.                                         MeshCommandPass.DispatchDraw(nullptr, RHICmdList, &InstanceCullingDrawParams);
  41.                                 }
  42.                 }
  43.        
  44. }
复制代码
上面的代码块整理了关键部门,也加了注释。首先,遍历所有VSM,初始化对应View视图的参数与矩阵、插手InstanceCulling,使用GPU进行裁剪,然后调用ProjectedShadowInfo->GetShadowDepthPass()进行DispatchDraw倡议DrawCall。
前面初始化部门,可以看到ProjectedShadowInfo里MeshDrawCommand绑定的Shader为:/Engine/Private/ShadowDepthVertexShader.usf、/Engine/Private/ShadowDepthPixelShader.usf。接下来可以看看这两个Shader的实现。
4.2、ShadowDepthVertexShader

首先,Include了VSM相关的头文件:


然后,定义了VSM相关的方式:


这两个方式定义了,如何将WorldPos转换到VSM Page上。
下图即为Main函数的VSM部门,将WorldPos转换至VSM的Page位置,然后输出成果给后续PS:


截图中有2个红框,上面一个暗示,如果没有启用VSM,那么直接输出计算的WorldPos,否则需要调用TransformToVirtualSmPage,将成果Transform到VSM Page对应的位置上,得到PositionClip与ClipPlanesInOut。
4.3、ShadowDepthPixelShader



将VS得到的Position通过ShadowDecodePageTable去PageTable取到物理页地址,然后调用InterlockedMax将更近的Depth写入到对应的物理页(VSM)内存里。
下图是RenderDoc截帧:



RenderDoc截帧,对应该函数部门衬着事件

首先是CullingPasses,使用CS对Primitives进行了裁剪。然后是RasterPasses光栅化,也就是正式将物体衬着到光源对应的ShadowDepthMap上。
做了个小测试,插手一个SpotLight,只照射到此中一个Cube,截帧如下图:



插手一个SpotLight

对比之前的截帧,RasterPasses多出了一个SM_Cube的ShadowDepth衬着,对应的就是该SpotLight的View视图在衬着Cube的Depth。

总结:分析了VSM的实现,VSM通过PageTable打点许多Page并将其映射到物理地址。VSM撑持高分辩率纹理,解决了暗影图分辩率的问题。它通过更小粒度(Page/Tile)的加载机制,无需为空闲Page分配物理空间,从而节省了内存开销;然后分析了一个光源如何把相关物体衬着到ShadowMap(VSM)上。衬着ShadowMap的大致过程为,以相机位置和光照标的目的构建LightViewMatrix,并以光源属性构建Viewport,然后在Shader里将WorldPosition变换到光照空间从而计算出光空间下WP对应的UV和Depth,计算出该UV地址Page位置,然后通过PageTable转换到物理地址,将成果写入VSM。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-21 22:26 , Processed in 0.163223 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表