最近光线追踪技术终于随着两大主机平台的支持进入了全面普及时代,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 Tracer和Path 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),保存被光子照亮的图像,最终与光栅化图像混合在一起
工作流程可以分成四个步骤:
- 根据任务缓存发射光子到场景中,一旦碰到不透明表面就把光子碰撞信息保存到光子缓存中,并把覆盖区域更新到反馈缓存。
- 在焦散缓存上绘制光子溅射:从光子缓存中取出一个光子根据参数绘制一个椭圆形光斑。
- 使用延迟渲染技术,把焦散缓存与场景图像混合在一起。
- 把当前帧的反馈缓存和上一帧的反馈缓存去重合并,产生下一帧所需的光线,存入任务缓存。
三 实现
焦散的实现代码散落在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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。