FineRIk 发表于 2024-7-15 18:25

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

前言:本篇是继“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:
//VirtualShadowMapPageManagement.usf
void CreateCachedPageMappings(uint VirtualShadowMapId, uint GlobalPageOffset, uint PageOffsetInSM, const bool bSinglePageSM)
{
        const int2 ClipmapCornerOffsetDelta = OffsetScale * ShadowMapCacheData.ClipmapCornerOffsetDelta;
        int2 PrevPageAddress = int2(PageAddress);
        if (Projection.LightType == LIGHT_TYPE_DIRECTIONAL)
        {
                // Clipmap panning
                PrevPageAddress += ClipmapCornerOffsetDelta;
        }
}VSM先分析到这里,接下来分析ShadowPass是怎么填充这些VSM数据的。

4、UE ShadowMap实现

初始化并分配需要衬着的VSM。InitViewsBeforePrepass,InitViewsAfterPrepass,InitDynamicShadows,BeginInitDynamicShadows这些方式城市执行一些VSM初始化操作,得到需要衬着的SortedShadowsForShadowDepthPass列表。下面是每帧执行的InitViewsAfterPrepass:
void Render(FRDGBuilder& GraphBuilder)
{
        void FDeferredShadingSceneRenderer::InitViewsAfterPrepass(/**/)
        {
                void FSceneRenderer::FinishInitDynamicShadows(/**/)
                {
                        // 遍历光源,初始化添加需要衬着的VSM
                        AllocateShadowDepthTargets(RHICmdList);
                        // 收集VSM的Primitive列表
                        // Generate mesh element arrays from shadow primitive arrays
                        void GatherShadowDynamicMeshElements(DynamicIndexBuffer, DynamicVertexBuffer, DynamicReadBuffer, InstanceCullingManager)
                        {//....
                                if (UseNonNaniteVirtualShadowMaps(ShaderPlatform, FeatureLevel))
                                {
                                        // GPUCULL_TODO: Replace with new shadow culling processor thing
                                        for (FProjectedShadowInfo* ProjectedShadowInfo : SortedShadowsForShadowDepthPass.VirtualShadowMapShadows)
                                        {
                                                if (ProjectedShadowInfo->bShouldRenderVSM)
                                                {
                                                        FVisibleLightInfo& VisibleLightInfo = VisibleLightInfos;
                                                        ProjectedShadowInfo->GatherDynamicMeshElements(*this, VisibleLightInfo, ReusedViewsArray, DynamicIndexBuffer, DynamicVertexBuffer, DynamicReadBuffer, InstanceCullingManager);
                                                }
                                        }
                                }
                                // 构建改VSM对应的MeshDrawCommand,绑定Shader,参数等
                                // FShadowDepthPassMeshProcessor,ShadowDepthVertexShader ShadowDepthPixelShader
                                SetupMeshDrawCommandsForShadowDepth(Renderer, InstanceCullingManager);
                        }
                }
        }
}从以上代码注释得出,首先遍历所有光源,分配VSM,然后收集每个VSM View相关的Primitives,使用FShadowDepthPassMeshProcessor生成MeshDrawCommand,供后续GPU剔除,光栅化使用。
衬着入口函数:FSceneRenderer::RenderShadowDepthMaps,调用关系为:
void FDeferredShadingSceneRenderer::Render(FRDGBuilder& GraphBuilder)
{
        void FSceneRenderer::RenderShadowDepthMaps(FRDGBuilder& GraphBuilder, FInstanceCullingManager &InstanceCullingManager)
        {
                // Ensure all shadow view dynamic primitives are uploaded before shadow-culling batching pass.
                // 更新这些Shadow影响到的Primitive到GPUScene
                for (FProjectedShadowInfo* ProjectedShadowInfo : SortedShadowsForShadowDepthPass.VirtualShadowMapShadows)
                {
                        Scene->GPUScene.UploadDynamicPrimitiveShaderDataForView(GraphBuilder, *Scene, *ProjectedShadowInfo->ShadowDepthView, ExternalAccessQueue, true);
                }
                // ....GPUScene其他Shadow更新
                InstanceCullingManager.BeginDeferredCulling(GraphBuilder, Scene->GPUScene);

                void FSceneRenderer::RenderVirtualShadowMaps(FRDGBuilder& GraphBuilder, bool bNaniteEnabled)
                {
                        void FShadowSceneRenderer::RenderVirtualShadowMaps(FRDGBuilder& GraphBuilder, bool bNaniteEnabled, bool bUpdateNaniteStreaming, bool bNaniteProgrammableRaster)
                        {
                                VirtualShadowMapArray.RenderVirtualShadowMapsNonNanite(GraphBuilder, VirtualShadowMapShadows, SceneRenderer.Views);
                                VirtualShadowMapArray.MergeStaticPhysicalPages(GraphBuilder);
                        }
                        // Render non-VSM shadows
                        RenderShadowDepthMapAtlases(GraphBuilder);
                }
        }
}首先,RenderShadowDepthMaps()的前半部门,将光源影响到的Primitive更新到GPUScene,然后使用之前分析的文章 GPUScene与InstanceCulling裁剪 对这些Primitives开启延迟裁剪,降低后续衬着暗影的Overdraw。
然后,调用VirtualShadowMapArray.RenderVirtualShadowMapsNonNanite()方式执行衬着。
void FVirtualShadowMapArray::RenderVirtualShadowMapsNonNanite(/**/)
{
        // 收集所有需要衬着的View:每个光源衬着ShadowDepth对应一个View,点光源6个方位对应为6个VSM的View
        TArray<Nanite::FPackedView, SceneRenderingAllocator> VirtualShadowViews;
        for (int32 Index = 0; Index < VirtualSmMeshCommandPasses.Num(); ++Index)
        {
                FVSMCullingBatchInfo VSMCullingBatchInfo;
                VSMCullingBatchInfo.FirstPrimaryView = uint32(VirtualShadowViews.Num());
                VSMCullingBatchInfo.NumPrimaryViews = AddRenderViews(ProjectedShadowInfo, 1.0f, HZBTexture != nullptr, false, ProjectedShadowInfo->ShouldClampToNearPlane(), VirtualShadowViews);
                VSMCullingBatchInfos.Add(VSMCullingBatchInfo);

                InstanceCullingMergedContext.AddBatch(GraphBuilder, InstanceCullingContext, DynamicInstanceIdOffset, ShadowDepthView->DynamicPrimitiveCollector.NumInstances(), &PassParameters->InstanceCullingDrawParams);
                // 添加到衬着列表
                BatchedVirtualSmMeshCommandPasses.Add(ProjectedShadowInfo);
        }

        // Instance合批
        InstanceCullingMergedContext.MergeBatches();
        // 开始遮挡剔除
        GraphBuilder.BeginEventScope(RDG_EVENT_NAME(”CullingPasses”));
        FCullingResult CullingResult = AddCullingPasses(/**/);
        // 光栅化,即衬着ShadowDepth
        GraphBuilder.AddPass(
                        RDG_EVENT_NAME(”RasterPasses”),
                        PassParameters,
                        ERDGPassFlags::Raster | ERDGPassFlags::SkipRenderPass,
                        [PassParameters,
                        BatchedPassParameters=MoveTemp(BatchedPassParameters),
                        BatchedVirtualSmMeshCommandPasses=MoveTemp(BatchedVirtualSmMeshCommandPasses)](FRHICommandList& RHICmdList)
                {
                                FIntRect ViewRect;
                                ViewRect.Min = FIntPoint(0, 0);
                                ViewRect.Max = FVirtualShadowMap::VirtualMaxResolutionXY;
                                FRHIRenderPassInfo RPInfo;
                                RPInfo.ResolveRect = FResolveRect(ViewRect);
                                RHICmdList.BeginRenderPass(RPInfo, TEXT(”RasterizeVirtualShadowMaps(Non-Nanite)”));

                                RHICmdList.SetViewport(ViewRect.Min.X, ViewRect.Min.Y, 0.0f, FMath::Min(ViewRect.Max.X, 32767), FMath::Min(ViewRect.Max.Y, 32767), 1.0f);
                                // 遍历所有需要衬着的VSM
                                for (int32 Index = 0; Index < BatchedVirtualSmMeshCommandPasses.Num(); ++Index)
                                {
                                        FParallelMeshDrawCommandPass& MeshCommandPass = ProjectedShadowInfo->GetShadowDepthPass();
                                        MeshCommandPass.DispatchDraw(nullptr, RHICmdList, &InstanceCullingDrawParams);
                                }
                }
       
}上面的代码块整理了关键部门,也加了注释。首先,遍历所有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。
页: [1]
查看完整版本: UE5衬着管线--ShadowPass通道与VSM