深入分析虚幻4引擎光线追踪(二):反射

(一) 概述

上一篇分析了虚幻4引擎光线追踪的整体架构,本篇开始详细拆解一个具体的光线追踪功能:反射。

注意:前方高能,非码农请迅速撤离!

首先是原理图:

光线追踪反射原理

(二) 渲染流程

光线追踪反射的流程是嵌入到天光的处理流程当中的,属于延迟处理的一部分,在渲染完成后使用加色模式叠加到场景颜色上,实际上可以看做是后处理的一部分,直接去掉也不会对原本的渲染产生任何不良影响。

大体可以分为以下步骤:

1) 加速结构构建

上一篇文章中提到,BVH加速结构的构建有两个步骤,以下加以详细说明。

  • 收集参与光追的物体 GatherRayTracingWorldInstances()

此函数遍历了场景中所有的FPrimitiveSceneInfo,把所有符合条件的索引添加到一个TArray当中;接下来遍历此TArray,处理每一个物体的LOD;再接下来重新遍历TArray,将无法并行收集的物体直接存入View.RayTracingGeometryInstances,构建并行收集结构。等待并行收集完成后,重新收集所有的AABB存入View.RayTracingAABBInstances用于构建BVH。

  • 更新加速结构 DispatchRayTracingWorldUpdates()

首先对收集到的物体构建加速结构,此处有同步和异步两种构建方式。

if (!bAsyncUpdateGeometry)
{
    ...
        //同步构建
        for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex)
        {
            FViewInfo& View = Views[ViewIndex];
            RHICmdList.BuildAccelerationStructure(View.RayTracingScene.RayTracingSceneRHI);
        }
}
else
{
    ...
        //异步构建
        for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex)
        {
            FViewInfo& View = Views[ViewIndex];
            RHIAsyncCmdList.BuildAccelerationStructure(View.RayTracingScene.RayTracingSceneRHI);
        }

        RHIAsyncCmdList.TransitionResource(EResourceTransitionAccess::ERWBarrier, EResourceTransitionPipeline::EComputeToGfx, nullptr, RayTracingDynamicGeometryUpdateEndFence);
        FRHIAsyncComputeCommandListImmediate::ImmediateDispatch(RHIAsyncCmdList);
}

此阶段会针对每一种光追功能进行初始化处理,对于光追反射,是调用PrepareRayTracingReflections()函数,此函数位于RayTracingReflections.cpp文件,主要初始化了用到的所有Shader。大部分光追功能会用到相同的shader,如RayTracingMaterialDefaultHitShaders.usf中的函数。

光追反射用到shader以及所在文件如下:

  • RayTracingReflectionsRGS() in RayTracingReflections.usf
  • OpaqueShadowCHS() in RayTracingMaterialDefaultHitShaders.usf
  • LightFunctionMaterialCallable() in RayTracingLightFunction.usf

另外引擎支持屏幕空间反射与光线追踪反射的混合模式,在此模式下光线追踪的反射次数将被强制限制为1次,下文将忽略混合模式,集中分析完全光线追踪模式的实现。

2 )光线生成

光线生成的调用放在FDeferredShadingSceneRenderer::RenderRayTracingReflections()函数中,实现位于RayTracingReflection.cpp文件。

光追反射实际上有三种模式:非延迟材质模式,延迟材质模式,以及体验版的延迟反射模式。函数在一开始先判断是否为体验版延迟反射模式:

    if (CVarRayTracingReflectionsExperimentalDeferred.GetValueOnRenderThread())
    {
        RenderRayTracingDeferredReflections( GraphBuilder, SceneTextures, View, OutDenoiserInputs);
        return;
    }

体验版延迟反射模式将在下文详细分析,而非延迟材质模式直接使用一个Pass完成,不需要下文提到的材质排序阶段,此处着重分析延迟材质模式。

另外从4.25版本开始,加入了ClearCoat的新材质,这使得情况更加复杂,以下分析将忽略ClearCoat材质和头发的针对性处理。

延迟材质模式下,引擎使用了两个Pass:

  • 收集(Gather)Pass,首先使用不透明模式(Opaque)追踪一次场景,得到第一次反射的表面信息,然后做一次材质排序
  • 着色(Shade)Pass,根据前一次着色标记的材质,从第一次反射的终点发射缩短的光线,可以处理诸如半透明这种特殊材质,最终进行着色

简化的代码如下:

//延迟材质模式下,使用2个Pass
const uint32 NumPasses = bSortMaterials ? 2 : 1;
//根据每个像素采样次数循环
for (int32 SamplePassIndex = 0; SamplePassIndex < SamplePerPixel; SamplePassIndex++)
{
    ...
    for (uint32 PassIndex = 0; PassIndex < NumPasses; ++PassIndex)
    {
        if (DeferredMaterialMode == EDeferredMaterialMode::Gather)
        {
             //收集Pass的光线追踪
            RHICmdList.RayTraceDispatch(Pipeline, RayGenShader.GetRayTracingShader(), RayTracingSceneRHI, GlobalResources, TileAlignedResolution.X, TileAlignedResolution.Y);
            //材质排序Pass
            SortDeferredMaterials(GraphBuilder, View, SortSize, DeferredMaterialBufferNumElements, DeferredMaterialBuffer);
        }
        else
        {
            if (DeferredMaterialMode == EDeferredMaterialMode::Shade)
            {
                //材质Buffer实际上是个1D的结构,排序过程将不可用的项移动到队列末尾
                //着色Pass使用排序过的材质可以最大程度减少输出的像素数量
                RHICmdList.RayTraceDispatch(View.RayTracingMaterialPipeline, RayGenShader.GetRayTracingShader(), RayTracingSceneRHI, GlobalResources, DeferredMaterialBufferNumElements, 1);
            }
            else
            {
                //非延迟模式
                RHICmdList.RayTraceDispatch(View.RayTracingMaterialPipeline, RayGenShader.GetRayTracingShader(), RayTracingSceneRHI, GlobalResources, RayTracingResolution.X, RayTracingResolution.Y);
            }
        }
    }

完成后将结果存入假反射G-Buffer,用于混合其他处理。

3) 材质排序

收集Pass输出的像素是散落在Buffer当中的,如果直接遍历则会产生很多无效的空隙,因此将有效的项根据材质属性放在一起,一来可以减少并行单元状态切换开销,二来可以最大化利用Buffer空间。

该排序实际上是个Compute Shader,函数名为MaterialSortLocal(),位于MaterialSort.usf。

排序算法本身是个简单的桶排序,除去针对GPU并行所做的优化外,并没有特别的内容,此处不再展开。

4) 分离假反射G-Buffer

此阶段构建一个假想的(imaginary)反射G-Buffer,用于降噪处理(Denoise),需要使用三张贴图:

  • 世界空间法线贴图
  • 深度贴图
  • 速度贴图, 此处的速度是假想的光线追踪碰撞点的速度

该过程也是一个Compute Shader,函数名为MainCS(),位于SplitImaginaryReflectionGBufferCS.usf。

此过程只是把光线追踪的结果存入三张贴图,此处不再展开分析。

5) 混合环境反射与天光

最终与场景渲染Buffer混合是在RenderDeferredReflectionsAndSkyLighting()函数中,位于文件IndirectLightRendering.cpp。

此过程除降噪外,使用了一个Pixel Shader混合环境反射与天光,函数名为ReflectionEnvironmentSkyLighting(),位于ReflectionEnvironmentPixelShader.usf,与延迟渲染处理方法相同,光线追踪反射和屏幕空间反射共用了一张贴图,作为参数传入该Shader,最终叠加混合到场景颜色Buffer上。

if (GetReflectionEnvironmentCVar() == 2 || GAOOverwriteSceneColor)
{
    //DEBUG模式直接覆盖场景颜色
    GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI();
}
else
{
    if (bCheckerboardSubsurfaceRendering)
    {
        //棋盘格渲染仅使用RGB通道,加色模式
        GraphicsPSOInit.BlendState = TStaticBlendState<CW_RGB, BO_Add, BF_One, BF_One>::GetRHI();
    }
    else
    {
        //加色模式
        GraphicsPSOInit.BlendState = TStaticBlendState<CW_RGBA, BO_Add, BF_One, BF_One, BO_Add, BF_One, BF_One>::GetRHI();
    }
}

(三) 算法分析

本节分析一下光线追踪Shader中的算法。调用的函数为RayTracingReflectionsRGS(),位于文件RayTracingReflections.usf。

该Shader中使用宏分离部分逻辑,给代码分析带来一定困难,由于收集Pass和着色Pass大部分代码类似,以下的分析忽略掉宏开关,统一使用逻辑判断代替,具体请参考Shader的源代码。

首先在忽略掉不可用的项以提高性能:

if (DIM_DEFERRED_MATERIAL_MODE == DEFERRED_MATERIAL_MODE_SHADE)
{
    //着色模式直接判断项目是否可用
    if (DeferredMaterialPayload.SortKey == RAY_TRACING_DEFERRED_MATERIAL_KEY_INVALID)
    {
        return;
    }
}

//过滤掉无限远的点,比如天空背景
float DeviceZ = SceneDepthBuffer.Load(int3(PixelCoord, 0)).r;
bool IsFiniteDepth = DeviceZ > 0.0;
if (!IsFiniteDepth)
{
    LocalSamplesPerPixel = 0;
}

//收集Pass过滤掉超出排序tile的项目
if (DIM_DEFERRED_MATERIAL_MODE == DEFERRED_MATERIAL_MODE_GATHER && any(DispatchThreadId >= RayTracingResolution))
{
    LocalSamplesPerPixel = 0;
}

//根据粗糙度计算淡出参数
float RoughnessFade = GetRoughnessFade(GBufferData.Roughness, ReflectionMaxRoughness);

//过于粗糙的普通表面不再计算反射
bool bIsValidPixel = (RoughnessFade > 0) && GBufferData.ShadingModelID != SHADINGMODELID_UNLIT;
if (bIsValidPixel)
{
    //发射光线与处理
}

光线发射使用了场景G-Buffer的信息来计算BRDF,以此来计算基于物理的反射光线:

//循环直到最大反弹次数
for (; BounceIndex < LocalMaxBounces; ++BounceIndex)
{
    if (BounceIndex == 0)
    {
        //第一次反弹特殊处理
        if (DIM_DEFERRED_MATERIAL_MODE == DEFERRED_MATERIAL_MODE_GATHER)
        {
            //收集Pass 只反弹一次,直接结束循环
            TraceRay(...); //发射光线
            break;
        }
        else if (DIM_DEFERRED_MATERIAL_MODE == DEFERRED_MATERIAL_MODE_SHADE)
        {
            ...
        }
    }
    
    //发射光线
    TraceMaterialRayPacked(...);
    
    //处理半透明
    if (EnableTranslucency)
    {
        Transmission = Transmit(...);
    }
    
    //积累结果
    AccumulateResults(...);
    
    //计算标志位和值
    if (PackedPayload.IsHit())
    {
        //碰撞到三角形
        ...
    }
    else
    {
        ...
    }
    
    //建立下一次迭代
    ...
    //未碰撞到或是碰撞到天空盒,结束循环    
    if (all(PathThroughput < 0.001) || bSkyWasHit || ClosestHitDistance<0.0f) break;
    if (TestPathRoughness)
    {
        ...
        //光滑度不足直接结束循环
        if (RoughnessFade <= 0.0f) break;
    }
}

最后输出颜色:

//判断采样项目是否可用
if (bIsValidSample)
{
    bNeedsCapture |= BounceIndex == LocalMaxBounces && !(bSkyWasHit || ClosestHitDistance < 0.0f);
    if (UseReflectionCaptures && bNeedsCapture)
    {
        //最终积累所有反射,得到最终结果
        float3 R = reflect(TopLayerRay.Direction, TopLayerWorldNormal);
        const float NoV = saturate(dot(-TopLayerRay.Direction, TopLayerWorldNormal));
        const float AO = 1.0f; //忽略AO
        const float RoughnessSq = TopLayerRoughness * TopLayerRoughness;
        const float SpecularOcclusion = GetSpecularOcclusion(NoV, RoughnessSq, AO);
        //计算环境BRDF
        PathRadiance.rgb += EnvBRDF(TopLayerSpecularColor, TopLayerRoughness, NoV) * SpecularOcclusion * PathThroughput *
                        CompositeReflectionCapturesAndSkylight(...);
    }
}

再接下来需要生成降噪使用的参数,此处略去。

整体的算法还是基于物理的模型,并且可以支持诸如天光、环境反射、半透明焦散等的混合。

(四) 延迟光线追踪反射(体验版)

(紧张修改中...)

(五)总结

本来我很喜欢最后再总结两句,有种大佬的感觉,可惜写完上文真的不知道该说什么了...... UNREAL Yes就完了!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容