乐趣区

关于算法:深入分析NVIDIA实时光线追踪焦散技术

最近光线追踪技术终于随着两大主机平台的反对进入了全面遍及时代,AMD 也紧跟 NVIDIA 的脚步推出了反对光追性能的新一代 GPU,光线追踪曾经成为了以后的一个技术热点。

本文将深入分析 NVIDIA 基于 UnrealEngine4 实现的实时光线追踪折射 / 焦散(Real-Time Ray-Tracing Refraction/Caustics)技术。因为 NV 的代码简直没有正文,以及集体程度无限,错漏之处在劫难逃,还望各路英雄不吝赐教。

一 原理


图 0 UnrealEngine4 通过 DXR 反对 NVIDIA RTX 技术

UnrealEngine4 应用微软 DXR 和 NVIDIA RTX 硬件实现了光线跟踪渲染,DXR 是微软 Direct3D12 图形 API 的一个组成部分,RTX 则是 NVIDIA 反对光线追踪的图形设施的名称。

NVIDIA 保护了 UnrealEngine4 的一个公有分支(https://github.com/NvRTX/UnrealEngine 请留神该分支为公有,须要 UnrealEingine4 以及 NVIDIA GameWorks 的拜访权限),次要目标是为引擎的光追成果集成专有技术 DLSS。本篇只关注该分支的一个非凡版本:对焦散的反对。

UnrealEngine4 曾经实现的光线追踪个性蕴含以下内容:【1】

光线追踪个性(Ray tracing feature) 光栅化等效(Raster equivalent)
光线追踪反射(Ray tracing reflections) (SSR or cube maps)
光线追踪暗影(Ray tracing shadows) (Shadow maps or DFS)
光线追踪环境遮蔽(Ray tracing ambient occlusion) (SSAO or DFAO)
光线追踪半透明(Ray tracing translucency) (Raster translucency)
光线追踪天光(Ray tracing skylight) (Raster skylight)
光线追踪全局照明(Ray tracing global illumination) (SSGI or LPV)

实际上 UnrealEngine4 蕴含两个光线追踪器,Ray TracerPath Tracer,其中 Ray Tracer 是一个与现有光栅化管线混合的追踪器,而 Path Tracer 则与电影级离线渲染器相似,目前并不能齐全的实时运行。如无特地阐明,本文内容全副针对 Ray Tracer 追踪器。

在 UnrealEngine4 中开启实时光线追踪,须要给场景增加一个后处理体积 (Post Process Volume),实际上 UnrealEngine 把所有光追特效看做是后处理的一部分,通常是将光线追踪个性渲染到一个离屏外表(Off-Screen Surface) 上,再与光栅化渲染的场景混合到一起。这样的做法有两个长处,一是光线追踪的精度与画面精度拆散,能够通过升高光追精度来减速;二是能够沿用光栅化管线的既有性能,使得光追实现艰难或者计算耗费大的个性能够在光栅化管线实现。【2】


图 1 UnrealEngine4 中的 Post-Process Volume 设置

实际上当初的游戏引擎正在趋同,Unity 也基于相似的原理实现了 DXR 光线追踪渲染,通过替换原有的光栅化管线中的后处理流程来实现雷同的性能。【8】


图 2 Unity 引擎的混合光线追踪引擎架构

NVIDIA 的 UE4 分支次要是集成专有的深度学习超采样 (DLSS) 技术和对光线追踪做贴近硬件的性能优化,其中有一个比拟乏味的个性,就是实时光线追踪焦散成果 (Ray Traced Caustic Effect)。实际上 NVIDIA 实现了两种焦散成果,一个是比拟通用的网格焦散(Mesh Caustic),另一个是非凡优化过的水面焦散(Water Caustic),因为水面焦散是一般焦散的一个极其特例,本文仅就网格焦散做一下剖析。

图 3 焦散的标志性特效:三棱锥色散

网格焦散应用的办法叫做光子溅射 (Photon Splatting),是光子贴图(Photon Mapping) 的一个变种实现。【3】

原始的光子贴图应用的办法有两个阶段:跟踪阶段(Tracing) —— 从光源地位跟踪光子穿过场景的门路;密度估计阶段(Density Estimation) —— 在表面上给定的点四周计算光子的密度,用于光照信息重建。【6】【7】在 NVIDIA 的办法中,第二个阶段被替换为光子溅射:在光子碰撞到不通明外表后应用加色混合的办法间接把色彩绘制进去。这样做有几个劣势,能够应用 GPU 的光栅化能力减速计算并且存储光子的构造非常简单,还有最重要的是,能够用很低的计算耗费进行光子微分(Photon Differentials,一种进步图像品质的数学方法)。光子微分算法能够针对每条光线跟踪它们的渺小扰动,而后用于预计光子的大小和形态,这样就能够在非镜面反射的外表产生椭圆形的光斑。通过应用各向异性的光斑设置,算法就能够在低采样率下重建焦散模型。

二 架构

为了整套机制可能实时渲染,NVIDIA 简化了光子追踪和微分计算,并应用了帧间反馈插值的办法来减速(Temporal Feedback,相似长期反走样 Temporal AA),NVIDIA 称之为 AAPS(Adaptive Anisotropic Photon Scattering,自适应各向异性光子溅射)。


图 4 AAPS 的工作流程

AAPS 应用了四种缓存:

  • 工作缓存(Task Buffer),保留以后帧须要跟踪的光线
  • 光子缓存(Photon Buffer),保留所有的光子信息
  • 反馈缓存(Feedback Buffer),保留最近几帧被光子笼罩的区域用于优化
  • 焦散缓存(Caustics Buffer),保留被光子照亮的图像,最终与光栅化图像混合在一起

工作流程能够分成四个步骤:

  1. 依据工作缓存发射光子到场景中,一旦碰到不通明外表就把光子碰撞信息保留到光子缓存中,并把笼罩区域更新到反馈缓存。
  2. 在焦散缓存上绘制光子溅射:从光子缓存中取出一个光子依据参数绘制一个椭圆形光斑。
  3. 应用提早渲染技术,把焦散缓存与场景图像混合在一起。
  4. 把以后帧的反馈缓存和上一帧的反馈缓存去重合并,产生下一帧所需的光线,存入工作缓存。

三 实现

焦散的实现代码散落在 UnrealEngine4 的 RayTraceRenderer 和相干 Shader 中,但并未扭转整体的光追架构。

最外围的实现代码在如下目录中:

EngineSourceRuntimeRendererPrivateRayTracingRayTracingMeshCaustics.cpp

NVIDIA 在 FDeferredShadingSceneRenderer 类中减少了一个函数:

 // NVCHANGE_BEGIN_YY : RT Caustics

 void RenderCausticsMapInner(FRHICommandListImmediate& RHICmdList, const FViewInfo& View, FIntPoint CausticsMapSize, FIntPoint CausticsMapViewSize);

实现的外围代码如下:

...

 // 增加重置数据 Pass
 AddResetDataPass(View, GraphBuilder, Buffers, DispatchIndirectParametersBufferUAV);

 // 查看调试选项
 int DebugType = 0;
 bool bIsDebugView = false;

 if (View.RayTracingRenderMode == ERayTracingRenderMode::RayTracingCausticsDebug && GetRayTracingDebugMode(View) == RAY_TRACING_DEBUG_VIZ_MESHCAUSTICS_DEBUG_DATA)
 {DebugType = GetLightDebugData(View);
 bIsDebugView = true;
 }

 // 增加光子跟踪 Pass
 AddPhotonTracingPass(View, LightParameters, GraphBuilder, Buffers, SceneDepthTexture, SceneMetallicTexture, SceneAlbedoTexture, PhotonBuffer, DispatchIndirectParametersBufferUAV, RectLightTextureArray, DebugType, EnableSoftCaustics);

 // 增加更新光线密度 Pass
 AddUpdateRayDensityPass(View, GraphBuilder, Buffers, DispatchIndirectParametersBuffer);

 // 增加光线生成 Pass
 AddGenerateRayQuadTreePass(View, GraphBuilder, Buffers);

 // 查看调试模式是否敞开了焦散
 if (GET_CAUSTICS_CMD_VAR(DebugDisablePhotonSplatting) == 0)
 {
 // 增加光子散射 Pass
 AddPhotonScatteringPass(View, GraphBuilder, PhotonBuffer, 
 SceneDepthTexture, SceneNormalTexture, SceneMetallicTexture, SceneAlbedoTexture, DispatchIndirectParametersBuffer, RTCausticsIntensityBuffer);

 // 增加长期过滤 Pass
 AddTemporalFilterPass(View, GraphBuilder, Buffers, SceneDepthTexture, SceneNormalTexture, RTCausticsIntensityBuffer);
 }

 // 增加焦散混合 Pass
 AddCompositeCausticsPass(*this, GraphBuilder, RHICmdList, SceneColorTexture, View, Buffers, SceneDepthTexture, SceneNormalTexture, SceneAlbedoTexture, RTCausticsIntensityBuffer, bIsDebugView);

 // 构建绘图
 Buffers.ReleaseBuffers();
 GraphBuilder.Execute();

波及批改的 Shader 十分之多,此处不一一列举,请参考我的项目的 Git 提交历史。实现折射的外围 Shader 放在了 EngineShadersPrivateRayTracingRayTracingMaterialHitShaders.usf 文件中:

...

// 判断是反射还是折射
if (isReflect)
{
 // 更新反射光微分参数
 UpdateReflectRayDifferential(v.Normal, hitData.dPdx, dNdx, hitData.dDdx);
 UpdateReflectRayDifferential(v.Normal, hitData.dPdy, dNdy, hitData.dDdy);
 // 失去反射光线方向和色彩
 R = reflect(rayDirW, v.Normal);
 hitData.color = F * hitData.color;
}
else
{
 // 获得折射向量
 GetRefractVector(rayDirW, v.Normal, R, eta);
 // 更新折射光线微分参数
 UpdateRefractRayDifferential(rayDirW, R, v.Normal, eta, hitData.dPdx, dNdx, hitData.dDdx);
 UpdateRefractRayDifferential(rayDirW, R, v.Normal, eta, hitData.dPdy, dNdy, hitData.dDdy);

// 判断是否开启了色散
#if REFRACTION_CAUSTICS_ENABLE_DISPERSION

 if(dispersionAmount != 0)
 hitData.SetDispersion(1);
#endif
 // 计算折射色彩
 float3 filterClr = Emissive + BaseColor;
 float filterAvgClr = max(filterClr.r, max(filterClr.g, filterClr.b));
 filterClr = filterClr / filterAvgClr;
 float transparency = 1 - Opacity;
 filterClr = lerp(filterClr, 1, transparency * transparency);
 hitData.color = filterClr * hitData.color;
}

计算色散的代码放在 EngineShadersPrivateRayTracingRayTracingMeshCaustics.usf 文件中:...

// 判断是否开启色散
if (EnableDispersion && hitData.IsDispersion())
{
 // 依据光线索引计算色散偏移
 uint3 launchIndex = DispatchRaysIndex();
 uint3 launchDimension = DispatchRaysDimensions();
 uint dispersionSample = DispersionSamples;
 uint taskIdx = (launchIndex.y * launchDimension.x + launchIndex.x);
 float dispersionOffset = float(taskIdx % dispersionSample) / (dispersionSample - 1);

 // 将偏移缩放到 [-1,1] 之间
 dispersionOffset = dispersionOffset * 2 - 1;

 // 计算色调偏移
 float greenWeight = float(dispersionSample + 1) / (2 * dispersionSample - 2);
 float3 colorWeight = saturate(float3(-dispersionOffset, (1 - abs(dispersionOffset)) * greenWeight, dispersionOffset));

 // 失去最终色彩和微分参数
 float lengthFactor = sqrt(float(dispersionSample));
 color *= colorWeight;// / dot(colorWeight, 0.333);
 dPdx *= lengthFactor;
 dPdy *= lengthFactor;
}

接下来拆分一下色散函数,其实是个非常简单的线性组合公式:

随着 x 增大,红色重量逐步缩小,蓝色重量则逐步增大,绿色重量则先增大再减小,最终色彩在七色色轮上挪动,造成色散。DispersionSamples 参数用来管制绿色重量的占比,因为人眼对绿色比拟敏感。

图 5 色散算法的图像

最终光子绘制则通过光栅化办法,绘制到焦散贴图上,再利用过滤算法对该贴图进行含糊,避免出现比拟硬的边缘,最终与场景图像混合在一起。波及到的 shader 如下:

图 6 焦散波及到的 shader

四 总结

NVIDIA 通过硬件加速的光线追踪实现了相当不错的焦散成果,然而在理论设施上运行时耗费还是十分大,尤其是场景复杂度减少当前。NVIDIA 官网的示例应用了反射探针,反射摄像机等传统的成果与之混合,如果仅仅应用光线追踪很难在足够的帧数下取得同样品质的图像。

能够预感在相当长的一段时间里,相似 RTX 版本 UnrealEngine4 这样混合光线追踪的做法将成为支流技术,咱们间隔残缺的光线追踪还有一段不小的间隔。【4】

参考文献

  • 【1】Introducing Ray Tracing in Unreal Engine 4 , NVIDIA
  • 【2】Real-time ray tracing in Unreal Engine, EPIC Games
  • 【3】An Introduction to Ray Traced Caustic Effects In Unreal Engine 4, NVIDIA
  • 【4】Are we done with Ray Tracing? SIGGRAPH2019 Alexander Keller (NVIDIA)
  • 【5】From Rasterization to Ray Tracing, SIGGRAPH2019 Morgan McGuire (NVIDIA)
  • 【6】Photon Mapping on Programmable Graphics Hardware, (2003) Stanford University/University of California
  • 【7】《Real-Time Rendering》www.realtimerendering.com
  • 【8】Leveraging Real-Time Ray Tracing To Build A Hybrid Game Engine, SIGGRAPH2019 Unity
退出移动版