概述
本系列文章旨在拆解虚幻4引擎中光线跟踪功能的具体实现。虚幻引擎是非常流行的游戏引擎,各种拆解文章汗牛充栋,在此不会对虚幻引擎的整体架构多做分析。由于本民科水平有限,难免错漏,还请各位看官海涵。
由于内容繁多,打算分成几篇来详细分析:
- 框架分析,即是本篇,主要是从框架抽象的角度来看整体设计
- 场景空间反射
- 全局照明(计划中)
- 阴影和全局遮蔽(计划中)
- 路径跟踪(计划中)
虚幻4延迟渲染框架
此处只分析虚幻4引擎的延迟渲染框架。虚幻4引擎通过TaskGraph实现了渲染逻辑的异步拆分(具体分析文章很多,此处不再赘述),其延迟渲染器的主体部分放在 DeferredShadingRenderer.cpp 文件中,具体逻辑在 FDeferredShadingSceneRenderer::Render() 函数中。虽然TaskGraph是完全异步的,但是整体的分派逻辑整合到了这个巨大的函数中,这并不利于定制修改和维护。
一般延迟渲染分为4个步骤:Z-Pass,前向阶段,延迟阶段,透明渲染和后处理【3】,虚幻引擎也并不例外。光线追踪部分则通过嵌入的方式合并到渲染中,最终在延迟渲染阶段与光栅化的图像合并在一起。
除PathTracing外,虚幻4引擎将光追拆分成了几个部分,分别计算,这样可以将计算量控制在一定范围,以达到实时处理的目的。
DXR的渲染框架
传统的光线跟踪渲染器可能各部分拆分的不是很明显,典型的如Blender集成的Cycles渲染器。【5】
DXR和RTX则很小心的把BVH射线求交部分拆分出来,使用硬件进行加速,开创了实时光线追踪的新时代。
DXR光线追踪可以大致分成以下模块:
BVH加速结构(Top Level和Bottom Level)
硬件加速的光线求交运算(硬件实现)
可编程的逻辑处理(Pipeline)
运算中临时存储结构(Shading Binding Table)
其中可编程部分DXR提供几种新的Shader,详情请查询DXR的官方文档,此处拆解一下其运算流程:
首先由光线产生Shader调用TraceRay()发射光线
进入硬件实现部分,对两级的BVH加速结构进行遍历
调用相交和碰撞的自定义Shader
判断是否有碰撞,调用最近碰撞或是未命中Shader
光栅化与光线追踪的混合
以上介绍了DXR的基本框架,在引擎里面临的现实问题是纯粹的光线跟踪对于本世代硬件来说还是太慢,无法真正的实时运行。因此虚幻4引擎采用了混合光栅化管线与光线追踪管线的方法,在最终的延迟/后处理阶段将两者合并在一起。
本文第一部分描绘了整个管线的框架,下面来具体看一下光线追踪反射与全局光照的调用逻辑,NVIDIA修改版的半透明焦散效果请参考本民科的 [此篇文章]:(https://www.jianshu.com/p/a238f22133d6)。
这一部分涉及的调用逻辑代码在IndirectLightRendering.cpp文件中。首先是在开始渲染前先要构建加速结构,在引擎中我们需要两个步骤:
首先要收集场景中参与光线追踪的所有物体
GatherRayTracingWorldInstances()
其次需要将物体的信息和状态同步加速结构中
DispatchRayTracingWorldUpdates()
然后是具体的调用:
void FDeferredShadingSceneRenderer::RenderDiffuseIndirectAndAmbientOcclusion(FRHICommandListImmediate& RHICmdListImmediate)
{
...
if (bApplyRTGI)
{
bool bIsValid = RenderRayTracingGlobalIllumination(GraphBuilder, SceneTextures, View, /* out */ &RayTracingConfig, /* out */ &DenoiserInputs);
if (!bIsValid)
{
DenoiseMode = 0;
}
}
...
}
void FDeferredShadingSceneRenderer::RenderDeferredReflectionsAndSkyLighting(FRHICommandListImmediate& RHICmdList, TRefCountPtr<IPooledRenderTarget>& DynamicBentNormalAO, TRefCountPtr<IPooledRenderTarget>& VelocityRT, FHairStrandsDatas* HairDatas)
{
...
if (bRayTracedReflections)
{
#if RHI_RAYTRACING
RayTracingConfig.RayCountPerPixel = GetRayTracingReflectionsSamplesPerPixel(View);
#else
RayTracingConfig.RayCountPerPixel = 0;
#endif
RenderRayTracingReflections(
GraphBuilder,
SceneTextures,
View,
RayTracingConfig.RayCountPerPixel, RayTracingConfig.ResolutionFraction,
&DenoiserInputs);
}
...
}
渲染的结果输出到指定的贴图上,最终通过加色混合模式叠加到帧缓存上。这个过程有时候会跟其他过程混合在一起,比如光线跟踪反射的结果是通过环境反射的Shader采样输出的,这样的好处是与屏幕空间反射可以无缝衔接到一起,减少不必要的消耗。
GraphBuilder.AddPass(
RDG_EVENT_NAME("ReflectionEnvironmentAndSky %dx%d", View.ViewRect.Width(), View.ViewRect.Height()),
PassParameters,
ERDGPassFlags::Raster,
[PassParameters, &View, PixelShader, bCheckerboardSubsurfaceRendering](FRHICommandList& InRHICmdList)
{
...
if (GetReflectionEnvironmentCVar() == 2 || GAOOverwriteSceneColor)
{
// override scene color for debugging
GraphicsPSOInit.BlendState = TStaticBlendState<>::GetRHI();
}
else
{
if (bCheckerboardSubsurfaceRendering)
{
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();
}
}
SetGraphicsPipelineState(InRHICmdList, GraphicsPSOInit);
SetShaderParameters(InRHICmdList, PixelShader, PixelShader.GetPixelShader(), *PassParameters);
FPixelShaderUtils::DrawFullscreenTriangle(InRHICmdList);
});
可以看到,实际上虚幻4的光线追踪是分离成不同的特性,最终叠加到光栅化管线的结果上的,这样做的好处是可以最大化利用原有的流程,修改不用伤筋动骨。
总结
由于要兼容各种配置的项目,代码中有大量的宏开关和条件判断,这部分功能也尚未完善(4.25版本),有大量的TODO并且缺乏注释,给分析带来了一些困难。最近使用最新的3060Ti显卡搭建Demo,发现性能还是十分喜人的,希望虚幻引擎能够尽快完善光追功能。
引用:
【3】Deferred Shading in S.T.A.L.K.E.R. (GPU Gems Chapter 9)
【5】Ray Tracing Deformable Scenes using Dynamic Bounding Volume Hierarchies