(一) 概述
上一篇分析了虛幻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就完了!