深入分析虛幻4引擎光線追蹤(二):反射

(一) 概述

上一篇分析了虛幻4引擎光線追蹤的整體架構(gòu),本篇開始詳細(xì)拆解一個(gè)具體的光線追蹤功能:反射。

注意:前方高能,非碼農(nóng)請(qǐng)迅速撤離!

首先是原理圖:

光線追蹤反射原理

(二) 渲染流程

光線追蹤反射的流程是嵌入到天光的處理流程當(dāng)中的,屬于延遲處理的一部分,在渲染完成后使用加色模式疊加到場景顏色上,實(shí)際上可以看做是后處理的一部分,直接去掉也不會(huì)對(duì)原本的渲染產(chǎn)生任何不良影響。

大體可以分為以下步驟:

1) 加速結(jié)構(gòu)構(gòu)建

上一篇文章中提到,BVH加速結(jié)構(gòu)的構(gòu)建有兩個(gè)步驟,以下加以詳細(xì)說明。

  • 收集參與光追的物體 GatherRayTracingWorldInstances()

此函數(shù)遍歷了場景中所有的FPrimitiveSceneInfo,把所有符合條件的索引添加到一個(gè)TArray當(dāng)中;接下來遍歷此TArray,處理每一個(gè)物體的LOD;再接下來重新遍歷TArray,將無法并行收集的物體直接存入View.RayTracingGeometryInstances,構(gòu)建并行收集結(jié)構(gòu)。等待并行收集完成后,重新收集所有的AABB存入View.RayTracingAABBInstances用于構(gòu)建BVH。

  • 更新加速結(jié)構(gòu) DispatchRayTracingWorldUpdates()

首先對(duì)收集到的物體構(gòu)建加速結(jié)構(gòu),此處有同步和異步兩種構(gòu)建方式。

if (!bAsyncUpdateGeometry)
{
    ...
        //同步構(gòu)建
        for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex)
        {
            FViewInfo& View = Views[ViewIndex];
            RHICmdList.BuildAccelerationStructure(View.RayTracingScene.RayTracingSceneRHI);
        }
}
else
{
    ...
        //異步構(gòu)建
        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);
}

此階段會(huì)針對(duì)每一種光追功能進(jìn)行初始化處理,對(duì)于光追反射,是調(diào)用PrepareRayTracingReflections()函數(shù),此函數(shù)位于RayTracingReflections.cpp文件,主要初始化了用到的所有Shader。大部分光追功能會(huì)用到相同的shader,如RayTracingMaterialDefaultHitShaders.usf中的函數(shù)。

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

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

另外引擎支持屏幕空間反射與光線追蹤反射的混合模式,在此模式下光線追蹤的反射次數(shù)將被強(qiáng)制限制為1次,下文將忽略混合模式,集中分析完全光線追蹤模式的實(shí)現(xiàn)。

2 )光線生成

光線生成的調(diào)用放在FDeferredShadingSceneRenderer::RenderRayTracingReflections()函數(shù)中,實(shí)現(xiàn)位于RayTracingReflection.cpp文件。

光追反射實(shí)際上有三種模式:非延遲材質(zhì)模式,延遲材質(zhì)模式,以及體驗(yàn)版的延遲反射模式。函數(shù)在一開始先判斷是否為體驗(yàn)版延遲反射模式:

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

體驗(yàn)版延遲反射模式將在下文詳細(xì)分析,而非延遲材質(zhì)模式直接使用一個(gè)Pass完成,不需要下文提到的材質(zhì)排序階段,此處著重分析延遲材質(zhì)模式。

另外從4.25版本開始,加入了ClearCoat的新材質(zhì),這使得情況更加復(fù)雜,以下分析將忽略ClearCoat材質(zhì)和頭發(fā)的針對(duì)性處理。

延遲材質(zhì)模式下,引擎使用了兩個(gè)Pass:

  • 收集(Gather)Pass,首先使用不透明模式(Opaque)追蹤一次場景,得到第一次反射的表面信息,然后做一次材質(zhì)排序
  • 著色(Shade)Pass,根據(jù)前一次著色標(biāo)記的材質(zhì),從第一次反射的終點(diǎn)發(fā)射縮短的光線,可以處理諸如半透明這種特殊材質(zhì),最終進(jìn)行著色

簡化的代碼如下:

//延遲材質(zhì)模式下,使用2個(gè)Pass
const uint32 NumPasses = bSortMaterials ? 2 : 1;
//根據(jù)每個(gè)像素采樣次數(shù)循環(huán)
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);
            //材質(zhì)排序Pass
            SortDeferredMaterials(GraphBuilder, View, SortSize, DeferredMaterialBufferNumElements, DeferredMaterialBuffer);
        }
        else
        {
            if (DeferredMaterialMode == EDeferredMaterialMode::Shade)
            {
                //材質(zhì)Buffer實(shí)際上是個(gè)1D的結(jié)構(gòu),排序過程將不可用的項(xiàng)移動(dòng)到隊(duì)列末尾
                //著色Pass使用排序過的材質(zhì)可以最大程度減少輸出的像素?cái)?shù)量
                RHICmdList.RayTraceDispatch(View.RayTracingMaterialPipeline, RayGenShader.GetRayTracingShader(), RayTracingSceneRHI, GlobalResources, DeferredMaterialBufferNumElements, 1);
            }
            else
            {
                //非延遲模式
                RHICmdList.RayTraceDispatch(View.RayTracingMaterialPipeline, RayGenShader.GetRayTracingShader(), RayTracingSceneRHI, GlobalResources, RayTracingResolution.X, RayTracingResolution.Y);
            }
        }
    }

完成后將結(jié)果存入假反射G-Buffer,用于混合其他處理。

3) 材質(zhì)排序

收集Pass輸出的像素是散落在Buffer當(dāng)中的,如果直接遍歷則會(huì)產(chǎn)生很多無效的空隙,因此將有效的項(xiàng)根據(jù)材質(zhì)屬性放在一起,一來可以減少并行單元狀態(tài)切換開銷,二來可以最大化利用Buffer空間。

該排序?qū)嶋H上是個(gè)Compute Shader,函數(shù)名為MaterialSortLocal(),位于MaterialSort.usf。

排序算法本身是個(gè)簡單的桶排序,除去針對(duì)GPU并行所做的優(yōu)化外,并沒有特別的內(nèi)容,此處不再展開。

4) 分離假反射G-Buffer

此階段構(gòu)建一個(gè)假想的(imaginary)反射G-Buffer,用于降噪處理(Denoise),需要使用三張貼圖:

  • 世界空間法線貼圖
  • 深度貼圖
  • 速度貼圖, 此處的速度是假想的光線追蹤碰撞點(diǎn)的速度

該過程也是一個(gè)Compute Shader,函數(shù)名為MainCS(),位于SplitImaginaryReflectionGBufferCS.usf。

此過程只是把光線追蹤的結(jié)果存入三張貼圖,此處不再展開分析。

5) 混合環(huán)境反射與天光

最終與場景渲染Buffer混合是在RenderDeferredReflectionsAndSkyLighting()函數(shù)中,位于文件IndirectLightRendering.cpp。

此過程除降噪外,使用了一個(gè)Pixel Shader混合環(huán)境反射與天光,函數(shù)名為ReflectionEnvironmentSkyLighting(),位于ReflectionEnvironmentPixelShader.usf,與延遲渲染處理方法相同,光線追蹤反射和屏幕空間反射共用了一張貼圖,作為參數(shù)傳入該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();
    }
}

(三) 算法分析

本節(jié)分析一下光線追蹤Shader中的算法。調(diào)用的函數(shù)為RayTracingReflectionsRGS(),位于文件RayTracingReflections.usf。

該Shader中使用宏分離部分邏輯,給代碼分析帶來一定困難,由于收集Pass和著色Pass大部分代碼類似,以下的分析忽略掉宏開關(guān),統(tǒng)一使用邏輯判斷代替,具體請(qǐng)參考Shader的源代碼。

首先在忽略掉不可用的項(xiàng)以提高性能:

if (DIM_DEFERRED_MATERIAL_MODE == DEFERRED_MATERIAL_MODE_SHADE)
{
    //著色模式直接判斷項(xiàng)目是否可用
    if (DeferredMaterialPayload.SortKey == RAY_TRACING_DEFERRED_MATERIAL_KEY_INVALID)
    {
        return;
    }
}

//過濾掉無限遠(yuǎn)的點(diǎn),比如天空背景
float DeviceZ = SceneDepthBuffer.Load(int3(PixelCoord, 0)).r;
bool IsFiniteDepth = DeviceZ > 0.0;
if (!IsFiniteDepth)
{
    LocalSamplesPerPixel = 0;
}

//收集Pass過濾掉超出排序tile的項(xiàng)目
if (DIM_DEFERRED_MATERIAL_MODE == DEFERRED_MATERIAL_MODE_GATHER && any(DispatchThreadId >= RayTracingResolution))
{
    LocalSamplesPerPixel = 0;
}

//根據(jù)粗糙度計(jì)算淡出參數(shù)
float RoughnessFade = GetRoughnessFade(GBufferData.Roughness, ReflectionMaxRoughness);

//過于粗糙的普通表面不再計(jì)算反射
bool bIsValidPixel = (RoughnessFade > 0) && GBufferData.ShadingModelID != SHADINGMODELID_UNLIT;
if (bIsValidPixel)
{
    //發(fā)射光線與處理
}

光線發(fā)射使用了場景G-Buffer的信息來計(jì)算BRDF,以此來計(jì)算基于物理的反射光線:

//循環(huán)直到最大反彈次數(shù)
for (; BounceIndex < LocalMaxBounces; ++BounceIndex)
{
    if (BounceIndex == 0)
    {
        //第一次反彈特殊處理
        if (DIM_DEFERRED_MATERIAL_MODE == DEFERRED_MATERIAL_MODE_GATHER)
        {
            //收集Pass 只反彈一次,直接結(jié)束循環(huán)
            TraceRay(...); //發(fā)射光線
            break;
        }
        else if (DIM_DEFERRED_MATERIAL_MODE == DEFERRED_MATERIAL_MODE_SHADE)
        {
            ...
        }
    }
    
    //發(fā)射光線
    TraceMaterialRayPacked(...);
    
    //處理半透明
    if (EnableTranslucency)
    {
        Transmission = Transmit(...);
    }
    
    //積累結(jié)果
    AccumulateResults(...);
    
    //計(jì)算標(biāo)志位和值
    if (PackedPayload.IsHit())
    {
        //碰撞到三角形
        ...
    }
    else
    {
        ...
    }
    
    //建立下一次迭代
    ...
    //未碰撞到或是碰撞到天空盒,結(jié)束循環(huán)    
    if (all(PathThroughput < 0.001) || bSkyWasHit || ClosestHitDistance<0.0f) break;
    if (TestPathRoughness)
    {
        ...
        //光滑度不足直接結(jié)束循環(huán)
        if (RoughnessFade <= 0.0f) break;
    }
}

最后輸出顏色:

//判斷采樣項(xiàng)目是否可用
if (bIsValidSample)
{
    bNeedsCapture |= BounceIndex == LocalMaxBounces && !(bSkyWasHit || ClosestHitDistance < 0.0f);
    if (UseReflectionCaptures && bNeedsCapture)
    {
        //最終積累所有反射,得到最終結(jié)果
        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);
        //計(jì)算環(huán)境BRDF
        PathRadiance.rgb += EnvBRDF(TopLayerSpecularColor, TopLayerRoughness, NoV) * SpecularOcclusion * PathThroughput *
                        CompositeReflectionCapturesAndSkylight(...);
    }
}

再接下來需要生成降噪使用的參數(shù),此處略去。

整體的算法還是基于物理的模型,并且可以支持諸如天光、環(huán)境反射、半透明焦散等的混合。

(四) 延遲光線追蹤反射(體驗(yàn)版)

(緊張修改中...)

(五)總結(jié)

本來我很喜歡最后再總結(jié)兩句,有種大佬的感覺,可惜寫完上文真的不知道該說什么了...... UNREAL Yes就完了!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容