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